Ronny
Ronny

Reputation: 481

Detect when elements within a scrollable div are out of view

I need to find a good solution to the following problem. I see a lot of people asking about tracking if an element is in or outside of view Port for the page or browser window. I need to be able to replicate this action, but inside a DIV that scrolls, with overflow:scroll for example. Does anyone know of a good example, for this specific action?

Thanks in advance.

Upvotes: 33

Views: 40684

Answers (9)

derpedy-doo
derpedy-doo

Reputation: 3036

Here's a solution using the relatively new Intersection Observer API (which has been supported in major browsers since 2019: https://caniuse.com/intersectionobserver).

This handles almost all edge cases I've seen for this question. (Off hand the only edge case it doesn't catch is when an absolutely positioned element is obscuring the scrolling elements.)

async function isElementInScrollView(element) {
    return new Promise((resolve) => {
        const observer = new IntersectionObserver(
            (entries, observerItself) => {
                observerItself.disconnect();
                resolve(entries[0].intersectionRatio === 1);
            }
        );
        observer.observe(element);
    });
}

usage:

const isInView = await isElementInScrollView(document.querySelector('#my-element'));

You can modify the intersectionRatio === 1 part of the function implementation to change how "in view" the element must be in order for this function to return true. (intersectionRatio is a value between 0.0 and 1.0.) See intersectionRatio docs here: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/intersectionRatio

Here's an example:

async function isElementInScrollView(element) {
  return new Promise((resolve) => {
    const observer = new IntersectionObserver((entries, observerItself) => {
      observerItself.disconnect();
      resolve(entries[0].intersectionRatio === 1);
    });
    observer.observe(element);
  });
}

async function detect() {
  const allChildren = Array.from(document.querySelectorAll(".child"));

  const results = await Promise.all(
    allChildren.map(async(child) => {
      return await isElementInScrollView(child);
    })
  );

  printResults(results);
}

function printResults(results) {
  document.querySelector('.results').innerHTML = results.join('<br>')
}

detect();
.restricted {
  padding: 32px;
  padding-right: 64px;
  border: 1px solid black;
  max-height: 100px;
  width: 100px;
  overflow: auto;
  display: inline-flex;
  flex-direction: column;
  gap: 8px;
}

.child {
  padding: 8px;
  border: 1px solid red;
}

.results {
  display: inline-block;
}

body>* {
  vertical-align: top;
}
<div class="restricted">
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
  <div class="child">Child</div>
</div>

<button onclick="detect()">Detect</button>
<div class="results"></div>

Upvotes: 3

Jony-Y
Jony-Y

Reputation: 1579

Played around with it for my purposes. Here is my solution (vanilla)

Menu is the container, el is the active element.

const isVisible = (menu, el) => {
      const menuHeight = menu.offsetHeight;
      const menuScrollOffset = menu.scrollTop;
    
      const elemTop = el.offsetTop - menu.offsetTop;
      const elemBottom = elemTop + el.offsetHeight;
      return (elemTop >= menuScrollOffset &&
     elemBottom <= menuScrollOffset + menuHeight);
    }

Upvotes: 2

Sarath Ak
Sarath Ak

Reputation: 8649

You can try this

function isScrolledIntoView(elem) {
    var docViewTop = $(window).scrollTop();
    var docViewBottom = docViewTop + window.innerHeight;
    var el = $(elem);
    var elemTop = el.offset().top;
    var elemBottom = elemTop + el.height();
    var elemDisplayNotNone = el.css("display") !== "none";

    return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop) && elemDisplayNotNone);
}

eg:

isScrolledIntoView('#button')

Upvotes: 0

palerdot
palerdot

Reputation: 7642

I was able to make this work by making a small change to the pure javascript version posted

function checkInView(container, element, partial) {

    //Get container properties
    let cTop = container.scrollTop;
    let cBottom = cTop + container.clientHeight;

    //Get element properties
    let eTop = element.offsetTop - container.offsetTop; // change here
    let eBottom = eTop + element.clientHeight;

    //Check if in view    
    let isTotal = (eTop >= cTop && eBottom <= cBottom);
    let isPartial = partial && (
      (eTop < cTop && eBottom > cTop) ||
      (eBottom > cBottom && eTop < cBottom)
    );

    //Return outcome
    return  (isTotal  || isPartial);
  }

Upvotes: 5

Jonny Ngan
Jonny Ngan

Reputation: 137

Based of the best answer. Instead of just telling you if an element is partially visible or not. I added a little extra so you can pass in a percentage (0-100) that tells you if the element is more than x% visible.

function (container, element, partial) {
    var cTop = container.scrollTop;
    var cBottom = cTop + container.clientHeight;
    var eTop = element.offsetTop;
    var eBottom = eTop + element.clientHeight;
    var isTotal = (eTop >= cTop && eBottom <= cBottom);
    var isPartial;

    if (partial === true) {
        isPartial = (eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom);
    } else if(typeof partial === "number"){
        if (eTop < cTop && eBottom > cTop) {
            isPartial = ((eBottom - cTop) * 100) / element.clientHeight > partial;
        } else if (eBottom > cBottom && eTop < cBottom){ 
            isPartial = ((cBottom - eTop) * 100) / element.clientHeight > partial;
        }
    }
    return (isTotal || isPartial);
}

Upvotes: 8

Abdul Alim
Abdul Alim

Reputation: 346

Here is a pure javascript solution.

function elementIsVisible(element, container, partial) {
    var contHeight = container.offsetHeight,
    elemTop = offset(element).top - offset(container).top,
    elemBottom = elemTop + element.offsetHeight;
    return (elemTop >= 0 && elemBottom <= contHeight) || 
    (partial && ((elemTop < 0 && elemBottom > 0 ) || (elemTop > 0 && elemTop <= contHeight)))
}

// checks window
function isWindow( obj ) {
    return obj != null && obj === obj.window;
}

// returns corresponding window
function getWindow( elem ) {
    return isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;
}

// taken from jquery
// @returns {{top: number, left: number}} 
function offset( elem ) {

    var docElem, win,
        box = { top: 0, left: 0 },
        doc = elem && elem.ownerDocument;

    docElem = doc.documentElement;

    if ( typeof elem.getBoundingClientRect !== typeof undefined ) {
        box = elem.getBoundingClientRect();
    }
    win = getWindow( doc );
    return {
        top: box.top + win.pageYOffset - docElem.clientTop,
        left: box.left + win.pageXOffset - docElem.clientLeft
    };
};

Upvotes: 1

Adam Reis
Adam Reis

Reputation: 4473

Here's a pure javascript version of the accepted answer without relying on jQuery and with some fixes to the partial in view detection and support for out of view on top.

function checkInView(container, element, partial) {

    //Get container properties
    let cTop = container.scrollTop;
    let cBottom = cTop + container.clientHeight;

    //Get element properties
    let eTop = element.offsetTop;
    let eBottom = eTop + element.clientHeight;

    //Check if in view    
    let isTotal = (eTop >= cTop && eBottom <= cBottom);
    let isPartial = partial && (
      (eTop < cTop && eBottom > cTop) ||
      (eBottom > cBottom && eTop < cBottom)
    );

    //Return outcome
    return  (isTotal  || isPartial);
}

And as a bonus, this function ensures the element is in view if it's not (partial or full):

function ensureInView(container, element) {

    //Determine container top and bottom
    let cTop = container.scrollTop;
    let cBottom = cTop + container.clientHeight;

    //Determine element top and bottom
    let eTop = element.offsetTop;
    let eBottom = eTop + element.clientHeight;

    //Check if out of view
    if (eTop < cTop) {
      container.scrollTop -= (cTop - eTop);
    }
    else if (eBottom > cBottom) {
      container.scrollTop += (eBottom - cBottom);
    }
}

Upvotes: 40

David
David

Reputation: 1

I made a jquery plugin with the last answer:

(function($) {
    $.fn.reallyVisible = function(opt) {

        var options = $.extend({
            cssChanges:[
                { name : 'visibility', states : ['hidden','visible'] }
            ],
            childrenClass:'mentioners2',
            partialview : true
        }, opt);

        var container = $(this);
        var contHeight;
        var contTop;
        var contBottom;
        var _this = this;
        var _children;

        this.checkInView = function(elem,partial){

            var elemTop = $(elem).offset().top - container.offset().top;
            var elemBottom = elemTop + $(elem).height();

            var isTotal = (elemTop >= 0 && elemBottom <=contHeight);
            var isPart = ((elemTop < 0 && elemBottom > 0 ) || (elemTop > 0 && elemTop <= container.height())) && partial ;

            return  isTotal  || isPart ;
        }

        this.bind('restoreProperties',function(){
            $.each(_children,function(i,elem){
                $.each(options.cssChanges,function(i,_property){
                    $(elem).css(_property.name,_property.states[1]);        
                });
            });
            _children = null;
        });

        return this.each(function(){
            contHeight = container.height();
            contTop = container.scrollTop();
            contBottom = contTop + contHeight ;

            _children = container.children("."+options.childrenClass);

            $.each(_children,function(i,elem){
                var res = _this.checkInView(elem,options.partialview);
                if(  !res ){
                    $.each(options.cssChanges,function(i,_property){
                        $(elem).css(_property.name,_property.states[0]);        
                    });
                }
            });

        }); 
    }

})(jQuery);

Upvotes: 0

Akram Berkawy
Akram Berkawy

Reputation: 5060

i had the same problem before, i have ended up with the following function.the first parameter is for the element to check, the second is to check if the element is partially in-view.it is for vertical check only, you can extend it to check for horizontal scroll.

function checkInView(elem,partial)
{
    var container = $(".scrollable");
    var contHeight = container.height();
    var contTop = container.scrollTop();
    var contBottom = contTop + contHeight ;

    var elemTop = $(elem).offset().top - container.offset().top;
    var elemBottom = elemTop + $(elem).height();

    var isTotal = (elemTop >= 0 && elemBottom <=contHeight);
    var isPart = ((elemTop < 0 && elemBottom > 0 ) || (elemTop > 0 && elemTop <= container.height())) && partial ;

    return  isTotal  || isPart ;
}

check it on jsFiddle .

Upvotes: 38

Related Questions