It is not easy for a JavaScript to find out if a certain HTML element is currently visible for the user, meaning it is in the viewport of the browser window. See the discussion on stackoverflow for the great variety of answers to this question.
After reading and trying out some of these answers I decided to create my own solution of this problem. In this Blog I will present that implementation, done in pure JavaScript(no jQuery; but mind that I do not care for browsers that go their own way; Webkit browsers will work with my code).
Why would we need such functionality? Go to one of the new HTML-5 sites and watch the background picture changing when you scroll down. This is done by tracking the visibility of HTML elements. As soon as a certain element gets into view, the background image is exchanged.
In modern browsers we have an HTML element function called getBoundingClientRect(). This returns a rectangle with top, left, bottom, right (and even width and height), which is relative to the browser's viewport. The viewport is that part of the HTML page which is currently visible according to the user's scroll-position.
var element = document.getElementById("some-id");
var viewPortRelativeLocation = element.getBoundingClientRect();
To find out whether a rectangle is visible or not we need to translate this rectangle to page-absolute coordinates. Then we could check if this absolute rectangle overlaps the current browser viewport.
Here is a function that delivers the browser's viewport in absolute coordinates (absolute means relative to the browser's client area, excluding scroll bars).
var browserViewPort = function() {
var rectangle = {
left: window.pageXOffset || document.documentElement.scrollLeft,
top: window.pageYOffset || document.documentElement.scrollTop,
width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
};
rectangle.right = rectangle.left + rectangle.width;
rectangle.bottom = rectangle.top + rectangle.height;
return rectangle;
};
This implementation is a concession to currently still sticky browser differences. I believe that in future document.documentElement.scrollLeft
and document.documentElement.clientWidth
will be the survivors. Always use a DRY implementation for such, then you can adapt it at any time to new browser standards.
Now to translate the element rectangle to absolute coordinates, we just need to add the browser rectangle to it.
var getAbsoluteRectangle = function(browserRect, elementRect) {
return {
left: elementRect.left + browserRect.left,
top: elementRect.top + browserRect.top,
right: elementRect.right + browserRect.left,
bottom: elementRect.bottom + browserRect.top
};
};
Seems that we are almost done, but complexity is awaiting behind the corner. I considered writing a separate Blog about intersecting segments, with discussion of all variants I found about it. But to be short I simply will provide an implementation for it.
Intersecting means that the rectangles somehow overlap.
var intersect = function(r1, r2) {
return r1.left < r2.right &&
r1.right > r2.left &&
r1.top < r2.bottom &&
r1.bottom > r2.top;
};
This surely can not be easily understood, but it works. I will deduce this later.
Another complexity is awaiting. An element could be nested within a scrolling container, like
<div style="height: 20em; overflow: auto;">
......
</div>
So we need to first check if the element is visible within its parent, and further going from parent to parent until, excluding, the document's body
. Only then we can check if the element's rectangle intersects the browser's viewport.
/** @return true when given element is at least partially visible, else false. */
var isVisible = function(element) {
var browser = browserViewPort();
var rect = getAbsoluteRectangle(browser, element.getBoundingClientRect());
/* could be in a scroll container, so check all parents */
var parent = element.parentNode;
while (parent && parent != document.body) {
var parentRectangle = getAbsoluteRectangle(browser, parent.getBoundingClientRect());
if ( ! intersect(parentRectangle, rect) || ! intersect(parentRectangle, browser))
return false;
parent = parent.parentNode;
}
return intersect(browser, rect);
};
First the browser's viewport is fetched, needed for translating rectangles to absolute coordinates. Then the element's absolute rectangle is calculated. Now a loop across all parents is started, whenever the element's rectangle does not intersect the parent, or the parent does not intersect the browser's viewport, false is returned. When this all was true, also the element's rectangle is checked to intersect the browser's viewport.
Now to perform the trick with changing background images we need to receive scroll events from the browser. Then we can use the function isVisible(element)
to find out if a certain element is (at least partially) visible.
Receiving the main scrollbar movements is easy, we just need to add "scroll" listeners to the global JS window
object. But for the nested scroll panes we need to know each of them to add a scroll listeners. You can use following code to install them.
var initializeTracking = function(eventCallback, scrollPanes) {
window.addEventListener("scroll", eventCallback);
window.addEventListener("resize", eventCallback);
if (scrollPanes)
for (var i = 0; i < scrollPanes.length; i++)
scrollPanes[i].addEventListener("scroll", eventCallback);
};
This accepts at least one, optionally two parameters. The first is your callback function, called any time some scrollbar is moved. The second can be an array of nested scroll panes to watch.
This is a solution that complies with following test scenarios ...
... and use cases ...
resize: both;
) in a way that makes nested elements invisible Have a look at my test page and you will understand what I mean. Try to carry out all described use cases with that page. On bottom of that page you find the current JS source code, and you can try out if this also works with the browser of your choice.
ɔ⃝ Fritz Ritzberger, 2015-08-14