25

Is there any technique or set of techniques that can be used to implement what, in effect, would be an onVisible 'event' in JavaScript?

I would like my JavaScript to detect when an element in a web page, such as a paragraph of text, or an image, becomes visible in a browser window as a user scrolls down a page. I would also like a corresponding 'event', onNotVisible, to fire when an element that was once visible in the browser window can no longer be seen.

If it cannot be easily implemented in JavaScript, are there any browser specific events that can provide the same functionality?

RobertG
  • 251
  • 1
  • 3
  • 3
  • may be this might help you http://stackoverflow.com/questions/399867/custom-events-in-jquery –  Sep 27 '10 at 17:44
  • 2
    see http://stackoverflow.com/questions/487073/jquery-check-if-element-is-visible-after-scroling – edbond Sep 27 '10 at 19:43

3 Answers3

9

I had to try this myself, and this is what I came up with:

<!DOCTYPE html>
<html>
<head>
<script>

var EventListener = function(element, callback) {
    this._el = element;
    this._cb = callback;
    this._at = false;
    this._hasBeenVisible = false;
    this._hasBeenInvisible = true;
    var  _me = this;

    window.onscroll = function() {
        for (q in EventListener.queue.onvisible) {
            EventListener.queue.onvisible[q].call();
        }
        for (q in EventListener.queue.oninvisible) {
            EventListener.queue.oninvisible[q].call();
        }
    };

    return {
        onvisible: function() {
            EventListener.queue.onvisible.push(function() {
                if (!_me._at && _me._hasBeenInvisible && (window.pageYOffset + window.innerHeight) > _me._el.offsetTop && window.pageYOffset < (_me._el.offsetTop + _me._el.scrollHeight)) {
                    _me._cb.call();
                    _me._at = true;
                    _me._hasBeenVisible = true;
                }
            });
            EventListener.queue.oninvisible.push(function() {
                if (_me._hasBeenVisible && ((window.pageYOffset + window.innerHeight) < _me._el.offsetTop || window.pageYOffset > (_me._el.offsetTop + _me._el.scrollHeight))) {
                    _me._hasBeenInvisible = true;
                    _me._hasBeenVisible   = false;
                    _me._at = false;
                }
            });
        },
        oninvisible: function() {
            EventListener.queue.oninvisible.push(function() {
                if (!_me._at && _me._hasBeenVisible && ((window.pageYOffset + window.innerHeight) < _me._el.offsetTop || window.pageYOffset > (_me._el.offsetTop + _me._el.scrollHeight))) {
                    _me._cb.call();
                    _me._at = true;
                    _me._hasBeenInvisible = true;
                }
            });
            EventListener.queue.onvisible.push(function() {
                if (_me._hasBeenInvisible && (window.pageYOffset + window.innerHeight) > _me._el.offsetTop && window.pageYOffset < (_me._el.offsetTop + _me._el.scrollHeight)) {
                    _me._hasBeenVisible = true;
                    _me._hasBeenInvisible = false;
                    _me._at = false;
                }
            });
        }
    };
}
EventListener.queue = {
    onvisible:   [],
    oninvisible: []
};

function addListener(element, event, fn) {
    if (typeof element == 'string')
        element = document.getElementById(element);

    var listener = new EventListener(element, fn);

    if (listener['on' + event.toLowerCase()])
        return listener['on' + event.toLowerCase()].call();
}

window.onload = function() {
    addListener('event-element', 'visible', function() {
        alert("Element One Visible!");
    });
    addListener('event-element', 'invisible', function() {
        alert("Element One Invisible!");
    });
    addListener('event2-element', 'visible', function() {
        alert("Element Two Visible!");
    });
    addListener('event2-element', 'invisible', function() {
        alert("Element Two Invisible");
    });
}

</script>
</head>
<body>

<h1>Hey!</h1>

<div style="height: 1500px;">
Please scroll down some pixels.
</div>

<p id="event-element">
    This element should cast an event 'onvisible' and 'oninvisible'.
</p>

<div style="height: 1000px;">

</div>

<p id="event2-element">
    Another one!
</p>


</body>
</html>

Tested in:

  • FireFox 3.6 (works)
  • Chrome 6.0.472 (works)
  • Opera 10.62 (works)
  • Safari 4 (Works in a very buggy and irritating way, don't use popups if you try it here!)

(The code is also available on PasteBin)

The above code is not optimized in any way, I just started writing and ended when it worked. Preferrably you would probably want to use only one queue, refactor some code and make it more generic. Provided as is.

Björn
  • 27,479
  • 9
  • 63
  • 81
  • 1
    good answer. This does not account for the display attribute though! One will need to traverse the element's parentNode up and check if all their display attributes are visible. Very useful for on-demand content loading. – Lorenz Lo Sauer Apr 30 '12 at 05:16
7

You'd have to set up an event listener for the document's or window's onscroll event. Then you'd measure the currently visible area by adding the clientHeight to scrollTop, and then you check if your element of choice is within that area. Something like this

myElem=document.getElementById('some_id');
scrollOffset=document.getElementsByTagName('html')[0].scrollTop;
visibleHeight=document.getElementsByTagName('html')[0].clientHeight;
if(myElem.offsetTop>=scrollOffset && myElem.offsetTop<=scrollOffset+visibleHeight){
    // element is in visible area
}

If that's the case you set some flag somewhere in your code.

You're likely to run into browser incompatibilities there though. So better use some library.

DanMan
  • 10,431
  • 3
  • 36
  • 57
4

One of the answers in the question linked above by @edbond gives a pointer to the jQuery appear plugin. This plugin allows you to attach appear and disappear callbacks to an element that are called as the element scrolls into and out of view, which sounds like exactly what you are looking for.

Community
  • 1
  • 1
David Hull
  • 1,105
  • 12
  • 17