简体   繁体   中英

Change active state on scroll to viewport

I'm trying to make a single static website, which when an div child of comes into viewport (precisely, when div element comes into the upper 50% of the viewport) changes the corresponding div's class in side-nav to "active". It should work scrolling down and up.

So far I've tried several solution from other threads on SO, none successful. I assume I've been approaching this wrong.

 $(window).on('scroll', function() { $("#vars-args").each(function() { if (elementInViewport2($(this))) { $(this).find("#div1a").addClass("active"); } }); }); function elementInViewport2(el) { var top = el.offsetTop; var left = el.offsetLeft; var width = el.offsetWidth; var height = el.offsetHeight; while (el.offsetParent) { el = el.offsetParent; top += el.offsetTop; left += el.offsetLeft; } return ( top < (window.pageYOffset + window.innerHeight) && left < (window.pageXOffset + window.innerWidth) && (top + height) > window.pageYOffset && (left + width) > window.pageXOffset ); } 
 <script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script> <div id="side-nav"> <a href="" id="div1a">1</a> <a href="" id="div2a">2</a> <a href="" id="div3a">3</a> <a href="" id="div4a">4</a> <a href="" id="div5a">5</a> <a href="" id="div6a">6</a> </div> <div id="content"> <div id="div1"> <!--content--> </div> <div id="div2"> <!--content--> </div> <div id="div3"> <!--content--> </div> <div id="div4"> <!--content--> </div> <div id="div5"> <!--content--> </div> <div id="div6"> <!--content--> </div> </div> 

Also note that content of each div inside can be larger than the size of viewport.

I have been having problems getting the javascript to work. Also please note that the current JS is copied from some other thread.

This can be achieved using the IntersectionObserver as told by @cloned in the comments: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

To achieve this, you need a callback function passed as a parameter which is executed once isIntersecting is true , an option object (below it sets the threshold at 50% of the element) and an IntersectionObserver .

The callback toggles the active class to the a element according to the entry's id .

At the end we loop through the divs and make our observer observe them.

const callback = (entries, observer) => {
entries.forEach(entry => {
  const navItem = document.querySelector('#' + entry.target.id + 'a');
    if (entry.isIntersecting) {
      console.log(navItem.getAttribute('id'));
      navItem.classList.add('active');
    } else {
      navItem.classList.remove('active');
    }
  });
};
const options = {
  threshold: 0.5
};
const observer = new IntersectionObserver(callback, options);
const container = document.getElementById('content');
const targetElements = container.querySelectorAll('div');


targetElements.forEach(element => {
 observer.observe(element);
});

Here is a JSBin to demonstrate it https://jsbin.com/riyuhediso/47/edit?html,js,console,output

Note that although it demonstrates its feasibility it's not been profiled for performance issues which can be significant so I don't vouch for it.

If you are using Bootstrap you can use the ScrollSpy lib https://getbootstrap.com/docs/4.1/components/scrollspy/ and there is also ScrollMagic which is great http://scrollmagic.io/

You need to filter out which element is inside the viewport with the help of .getBoundingClientRect() Checkout this

and check if any content has it's top and bottom within the half of the viewport ( window.innerHeight )

I took help of filter function to find out the index of contents that is within the built in function and set the .active class of the corresponding anchor.

Have a look at the snippet:

 var direction = 0; // a variable to keep track of scrolled position; $(window).on('scroll', function() { // check if window is scrolling up or down; if ($(window).scrollTop() > direction) { // if true, window scrolling scrolling down; $('#side-nav').find('a').removeClass('active'); // remove active class from all anchors $('#side-nav').find('a').eq( // .eq() selector helps to find elements with index number, and here we pass a filter to find the content that is within the viewport; $('#content').find('div').filter(function(index) { return this.getBoundingClientRect().y <= (window.innerHeight / 2) && this.getBoundingClientRect().y + this.getBoundingClientRect().height > window.innerHeight / 2; }).index() ).addClass('active'); // update the current scroll position now; direction = $(window).scrollTop(); } else { // if false, window scrolling scrolling up; $('#side-nav').find('a').removeClass('active'); // remove active class from all anchors $('#side-nav').find('a').eq( $('#content').find('div').filter(function(index) { return this.getBoundingClientRect().y < (window.innerHeight / 2) && this.getBoundingClientRect().y + this.getBoundingClientRect().height > window.innerHeight / 2; }).index() ).addClass('active'); // update the current scroll position now; direction = $(window).scrollTop(); } }); 
 * { margin: 0; padding: 0; } #side-nav { /* feel free to remove or change, only for testing */ position: fixed; width: 100%; top: 0; padding: 15px; } #side-nav a { /* feel free to remove, only for testing */ text-decoration: none; color: grey; margin-right: 5px; font-size: 24px; font-weight: bold; } #side-nav a.active { color: #000; /* sets color for the default active class */ } #content div { min-height: 600px; background-color: #cecece; border-bottom: 1px solid #fff; box-sizing: border-box; padding: 50px; } 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <div id="side-nav"> <a href="" id="div1a" class='active'>1</a> <!-- set a default class assuming the first one will be in viewport while window loads --> <a href="" id="div2a">2</a> <a href="" id="div3a">3</a> <a href="" id="div4a">4</a> <a href="" id="div5a">5</a> <a href="" id="div6a">6</a> </div> <div id="content"> <div id="div1"> <p>One</p> </div> <div id="div2"> <p>Two</p> </div> <div id="div3"> <p>Three</p> </div> <div id="div4"> <p>Four</p> </div> <div id="div5"> <p>Five</p> </div> <div id="div6"> <p>Six</p> </div> </div> 

I have found a solution to this problem. NOTE: changes in html side-nav div, from id to class for all links. I understand this is a bad fix, so I will leave this question open if any optimizations or better solutions can be found.

 <script> window.onload = function() { var vars = document.getElementById('vars-args'); var als = document.getElementById('aliases') $(window).on('scroll', function() { $('#content').find('div').each(function(){ if (eleInView(this)){ var ele = $(this).attr('id'); document.getElementsByClassName(ele)[0].classList.add("active"); }; if (!eleInView(this)){ console.log(this); var ele = $(this).attr('id'); document.getElementsByClassName(ele)[0].classList.remove("active"); }; }); } )}; function eleInView(el){ var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); //console.log(el.getBoundingClientRect().top); return (h < el.getBoundingClientRect().bottom && el.getBoundingClientRect().top < h); } </script> 
  <script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script> <div id="side-nav"> <a href="" class="div1">1</a> <a href="" class="div2">2</a> <a href="" class="div3">3</a> <a href="" class="div4">4</a> <a href="" class="div5">5</a> <a href="" class="div6">6</a> </div> <div id="content"> <div id="div1"> <!--content--> </div> <div id="div2"> <!--content--> </div> <div id="div3"> <!--content--> </div> <div id="div4"> <!--content--> </div> <div id="div5"> <!--content--> </div> <div id="div6"> <!--content--> </div> </div> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM