Romes
Romes

Reputation: 3118

How to detect if DOM element is partially out of the viewport

I'm wondering if anyone has an easy solution for this. I'm trying to detect if any part of a HTML element finds itself outside of the viewport. I've tried utilizing the following code:

$.fn.isOnScreen = function(){

    var win = $(window);

    var viewport = {
        top : win.scrollTop(),
        left : win.scrollLeft()
    };
    viewport.right = viewport.left + win.width();
    viewport.bottom = viewport.top + win.height();

    var bounds = this.offset();
    bounds.right = bounds.left + this.outerWidth();
    bounds.bottom = bounds.top + this.outerHeight();

Brought to you by Steven

I can only get this to work when the entire element is not viewable anymore, but I just need to know if part of the element is outside of the viewport.

When the element is outside of the viewport, then I'm putting a different class on it, so that it will shift to the left instead so that it is viewable again.

Something like:

if(elementIsPartiallyOutsideViewport) {
    ele.addClass('move-left');
}

Any ideas?

Upvotes: 13

Views: 7321

Answers (2)

Andrii Kovalenko
Andrii Kovalenko

Reputation: 2227

There is a solution with an Intersection Observer. An advantage of the solution is position-relative calculations are not required.

jQuery plugin code snippet: (plugin.js)

(function ($) {
    class IntersectionDetector {
        async isElementPartiallyOutOfViewport(element) {
            return new Promise((resolve) => {
                const callback = this.handleOutOfViewportObservation.bind(this, resolve);
                const options = {
                    root: null,
                    threshold: 0,
                }

                const observer = new IntersectionObserver(callback, options);
                observer.observe(element);
            })
        }

        handleOutOfViewportObservation(resolve, [entry], observer) {
            const element = entry.target;

            observer.unobserve(element);
            observer.disconnect();

            const ratios = new Map();
            ratios.set('inViewportCompletelyRatio', 1);
            ratios.set('outOfViewportCompletelyRatio', 0);
            ratios.set('actualRatio', entry.intersectionRatio);

            const precision = Math.pow(10, 2);
            for (const [name, prevRatio] of ratios) {
                const nextRatio = precision * prevRatio;

                ratios.set(name, nextRatio);
            }

            const actualRatio = ratios.get('actualRatio');
            const inViewportCompletelyRatio = ratios.get('inViewportCompletelyRatio');
            const outOfViewportCompletelyRatio = ratios.get('outOfViewportCompletelyRatio');

            const isOutOfViewportPartially =
                (actualRatio > outOfViewportCompletelyRatio) &&
                (actualRatio < inViewportCompletelyRatio);

            resolve(isOutOfViewportPartially);
        }
    }

    $.fn.isOnScreen = async function () {
        const elements = this;
        const promises = elements.map(async (index, element) => {
            const detector = new IntersectionDetector();

            return await detector.isElementPartiallyOutOfViewport(element);
        });

        const results = await Promise.all(promises);

        return results.every(result => result);
    };
}(jQuery));

Usage example: (main.js)

jQuery(async function ($) {
    const isOnScreen = await $('#element-to-check').isOnScreen();

    console.log(isOnScreen);
});

Explanation:

  1. jQuery operates nodes collection, thus a better option is to check every node in a collection. If every node in the collection is partially visible the function returns true.
  2. The observer has specified a root argument equal to null, which means the element's intersection is being detected relative to a browser window.
  3. The solution uses precision. Potentially, JavaScript can cause a mistake while processing floating point numbers. The idea is to compare integer parts of numbers instead of floats to avoid the incorrect result. For instance, there is a good answer to this issue: https://stackoverflow.com/a/50780164/11173494

The code tested for:

  1. jQuery 3.6.*
  2. Chrome 103.0.* / Firefox 108.0

Upvotes: 0

Zafar
Zafar

Reputation: 3434

Most of the browsers already support getBoundingClientRect() method. So you can try the following code.

function isElementInViewport (el) {
    var rect = el.getBoundingClientRect();
    return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&     
        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
}

You simply pass the element to the function and get false if element is not inside the viewport.

Usage.

if (!isElementInViewport(el)) {
    el.addClass('move-left');
}

Edit

Just an addition. You can get more info about getBoundingClientRect() function and the browser support in here

Upvotes: 22

Related Questions