5

I have a piece of jQuery code that adds a css class to elements when they are scrolled into the viewport and removes that class when they are scrolled out of the viewport.

So far the code works like this:

  • When an element is scrolled into the viewport, the class "inview" is added.
  • When an element is scrolled out of the viewport, the class "inview" is removed.

So far so good. But what I am trying to achieve is this:

Scrolling into view:

  • When an element is scrolled into the viewport from the bottom of the page, the class "inview-bottom" is added.
  • When an element is scrolled into the viewport from the top of the page, the class "inview-top" is added.

Scrolling out of view:

  • When an element is scrolled out of the viewport from the bottom of the page, the class "outview-bottom" is added.
  • When an element is scrolled out of the viewport from the top of the page, the class "outview-top" is added.

Cleaning up:

  • When an element is scrolled into the viewport from the top or bottom of the page, all "outview-*" classes should be removed.
  • When an element is scrolled out of the viewport from the top or bottom of the page, all "inview-*" classes should be removed.

It was suggested in a comment to use the Intersection Observer API and after reading more about it, I believe it presents the best approach to fulfill the requirements.

Here is my code (open in full page - the preview doesn't work well). You can also find the same code on jsFiddle.

function inView(opt) {
  if (opt.selector === undefined) {
    console.log('Valid selector required for inView');
    return false;
  }
  var elems = [].slice.call(document.querySelectorAll(opt.selector)),
    once = opt.once === undefined ? true : opt.once,
    offsetTop = opt.offsetTop === undefined ? 0 : opt.offsetTop,
    offsetBot = opt.offsetBot === undefined ? 0 : opt.offsetBot,
    count = elems.length,
    winHeight = 0,
    ticking = false;

  function update() {
    var i = count;
    while (i--) {
      var elem = elems[i],
        rect = elem.getBoundingClientRect();
      if (rect.bottom >= offsetTop && rect.top <= winHeight - offsetBot) {
        elem.classList.add('inview');
        if (once) {
          count--;
          elems.splice(i, 1);
        }
      } else {
        elem.classList.remove('inview');
      }
    }
    ticking = false;
  }

  function onResize() {
    winHeight = window.innerHeight;
    requestTick();
  }

  function onScroll() {
    requestTick();
  }

  function requestTick() {
    if (!ticking) {
      requestAnimationFrame(update);
      ticking = true;
    }
  }
  window.addEventListener('resize', onResize, false);
  document.addEventListener('scroll', onScroll, false);
  document.addEventListener('touchmove', onScroll, false);
  onResize();
}
inView({
  selector: '.viewme', // an .inview class will get toggled on these elements
  once: false, // set this to false to have the .inview class be toggled on AND off
  offsetTop: 180, // top threshold to be considered "in view"
  offsetBot: 100 // bottom threshold to be considered "in view"
});
.box {
  width: 100%;
  height: 50vh;
  margin-bottom: 10px;
  background: blue;
  opacity: 0;
  transition: opacity .2s ease;
}

.inview {
  opacity: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
doğukan
  • 23,073
  • 13
  • 57
  • 69
Jascha Goltermann
  • 1,074
  • 2
  • 16
  • 31
  • You should really use [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for this. This was designed to watch elements whenever they come into view and react to it. – cloned May 15 '20 at 10:05
  • Hi, thanks for the tip! I have really no idea what that is and how to use this. I was only able to put together the code above with a lot of help from others - I wouldn't be able to write or understand it myself.. Can you show me? – Jascha Goltermann May 15 '20 at 11:15
  • I have found this but I can't make it work for my code: https://jsfiddle.net/sublines/xcyaks4g/5/ – Jascha Goltermann May 15 '20 at 11:36

1 Answers1

5

The fiddle you provided works fine with few changes. You need to apply the observer to all elements for it to work.

See this example:

const config = {
  root: null,
  rootMargin: '0px',
  threshold: [0.1, 0.5, 0.7, 1]
};

let previousY = 0;
let previousRatio = 0;


let observer = new IntersectionObserver(function(entries) {
  entries.forEach(entry => {
    const currentY = entry.boundingClientRect.y
    const currentRatio = entry.intersectionRatio
    const isIntersecting = entry.isIntersecting
    const element = entry.target;

    element.classList.remove("outview-top", "inview-top", "inview-bottom", "outview-bottom");
    // Scrolling up
    if (currentY < previousY) {
      const className = (currentRatio >= previousRatio) ? "inview-top" : "outview-top";
      element.classList.add(className);

      // Scrolling down
    } else if (currentY > previousY) {
      const className = (currentRatio <= previousRatio) ? "outview-bottom" : "inview-bottom";
      element.classList.add(className);
    }

    previousY = currentY
    previousRatio = currentRatio
  })
}, config);

const images = document.querySelectorAll('.box');
images.forEach(image => {
  observer.observe(image);
});
.box {
  width: 100%;
  height: 50vh;
  margin-bottom: 10px;
  background: lightblue;
  transition: opacity .2s ease;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 3em;
}

[class*='inview'] {
  opacity: 1;
}

[class*='outview'] {
  opacity: 0.6;
}
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
Kalimah
  • 11,217
  • 11
  • 43
  • 80
  • Thanks for the help! I tried something with your code here: https://jsfiddle.net/u8bLp0en/ However, it sometimes reacts too soon. Can we change the code so that it will only add the "outview" class when only 50px of the element is still visible in the viewport and it also adds the "inview" class as soon as 50px of the element is visible inside the viewport? – Jascha Goltermann May 18 '20 at 09:47
  • You can change the values of `threshold: [0.2, 0.7, 1]` to either an array of values or a single value. These values, however, represent percentages (not pixels) when the callback will be called. So in this example the callback will be invoked when div is 20%, 70% and 100% scrolled. – Kalimah May 18 '20 at 10:31
  • So what exactly do those four values mean? Why four values? What happens when I change `[0.1, 0.5, 0.7, 1]` to `[0.1, 0.2, 0.3, 0.4]`? And is the percentage relative to the DIVs height or to the viewport height? – Jascha Goltermann May 18 '20 at 17:09
  • These values are percentages relevant to div height. It is when the observer decides to take action. You can add as many values between 0 and 1. You can read more about it in this link (under threshold): https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API. – Kalimah May 18 '20 at 17:17
  • Okay, but why are there four different values? Do they correspond to inview-top, inview-bottom, outview-top, outview-bottom? And if so, in which order? – Jascha Goltermann May 19 '20 at 07:41
  • The four values are just to make the process more seamless. They don't correspond to inview or outview. For example, if you change to `threshold: 0` that means as soon as even one pixel is visible, the callback will be run. But that means the callback will be run only one time so we add an array of percentages to run the callback multiple times when the div is visible (from top or bottom). – Kalimah May 19 '20 at 11:03
  • Hello, @Kalimah could you take a look at my question? https://stackoverflow.com/questions/75204977/intersectionobserver-to-create-a-lazy-load-images-with-data-srcset-and-imagekit – Sophie Jan 24 '23 at 11:55