Bounding rect of HTML element within scrolling element - javascript

I'm interested in getting the bounding rect of a HTML element within an scrolling (overflow:auto) div container. I've tried getBoundingClientRect() but this is always relative to the window. A DOMRect is not needed, I just wanna know the width of the viewport and the position of the element.
So for me the only solution seems to be to subtract the rect of the scrolling div. But this could get complicated as my final use case is working with shadow DOM and custom components.
<div style="overflow:auto;height:100px;position:absolute;top:50px;">
<div id="elem">
some content
</div>
<div style="height:100px;">
ignore
</div>
<div>
ignore
</div>
<div>
ignore
</div>
</div>
<script>
window.alert("top="+document.getElementById("elem").getBoundingClientRect().top);
</script>
In this example you can see the outer most div has overflow set but the bounding rect does not show 0 before scrolling but 50.
Example: https://jsfiddle.net/nvemtoyk/

Found a workaround but with some caveats. First, you have to traverse all parent elements until you find the viewport. Secondy, It's only working if the overflow div was already scrolled.
At least in my case the second is true because the overflow style is not visible in javascript at my custom element. Maybe in "legacy" HTML this is not the case.
getViewport(elem) {
// root element
if (elem === document.body) {
return document.documentElement;
}
// scrolling element (only working if already scrolled)
// maybe elem.style.overflow is available, but not in my case
else if (elem.scrollLeft > 0 || elem.scrollTop > 0) {
return elem;
}
// traverse
else {
return getViewport(elem.offsetParent);
}
}
getBoundingRect(element, viewport) {
// root element
if (viewport === document.documentElement) {
return element.getBoundingClientRect();
}
// relative to viewport
else {
var elRect = element.getBoundingClientRect();
var vpRect = viewport.getBoundingClientRect();
return {
bottom: elRect.bottom - vpRect.top,
height: elRect.height,
left: elRect.left - vpRect.left,
right: elRect.right - vpRect.left,
top: elRect.top - vpRect.top,
width: elRect.width
};
}
}

Related

How to determine the real visibility of an element? [duplicate]

This question already has answers here:
Check if element is visible in DOM
(27 answers)
Closed 6 years ago.
In JavaScript, how would you check if an element is actually visible?
I don't just mean checking the visibility and display attributes. I mean, checking that the element is not
visibility: hidden or display: none
underneath another element
scrolled off the edge of the screen
For technical reasons I can't include any scripts. I can however use Prototype as it is on the page already.
For the point 2.
I see that no one has suggested to use document.elementFromPoint(x,y), to me it is the fastest way to test if an element is nested or hidden by another. You can pass the offsets of the targetted element to the function.
Here's PPK test page on elementFromPoint.
From MDN's documentation:
The elementFromPoint() method—available on both the Document and ShadowRoot objects—returns the topmost Element at the specified coordinates (relative to the viewport).
I don't know how much of this is supported in older or not-so-modern browsers, but I'm using something like this (without the neeed for any libraries):
function visible(element) {
if (element.offsetWidth === 0 || element.offsetHeight === 0) return false;
var height = document.documentElement.clientHeight,
rects = element.getClientRects(),
on_top = function(r) {
var x = (r.left + r.right)/2, y = (r.top + r.bottom)/2;
return document.elementFromPoint(x, y) === element;
};
for (var i = 0, l = rects.length; i < l; i++) {
var r = rects[i],
in_viewport = r.top > 0 ? r.top <= height : (r.bottom > 0 && r.bottom <= height);
if (in_viewport && on_top(r)) return true;
}
return false;
}
It checks that the element has an area > 0 and then it checks if any part of the element is within the viewport and that it is not hidden "under" another element (actually I only check on a single point in the center of the element, so it's not 100% assured -- but you could just modify the script to itterate over all the points of the element, if you really need to...).
Update
Modified on_top function that check every pixel:
on_top = function(r) {
for (var x = Math.floor(r.left), x_max = Math.ceil(r.right); x <= x_max; x++)
for (var y = Math.floor(r.top), y_max = Math.ceil(r.bottom); y <= y_max; y++) {
if (document.elementFromPoint(x, y) === element) return true;
}
return false;
};
Don't know about the performance :)
As jkl pointed out, checking the element's visibility or display is not enough. You do have to check its ancestors. Selenium does this when it verifies visibility on an element.
Check out the method Selenium.prototype.isVisible in the selenium-api.js file.
http://svn.openqa.org/svn/selenium-on-rails/selenium-on-rails/selenium-core/scripts/selenium-api.js
Interesting question.
This would be my approach.
At first check that element.style.visibility !== 'hidden' && element.style.display !== 'none'
Then test with document.elementFromPoint(element.offsetLeft, element.offsetTop) if the returned element is the element I expect, this is tricky to detect if an element is overlapping another completely.
Finally test if offsetTop and offsetLeft are located in the viewport taking scroll offsets into account.
Hope it helps.
This is what I have so far. It covers both 1 and 3. I'm however still struggling with 2 since I'm not that familiar with Prototype (I'm more a jQuery type of guy).
function isVisible( elem ) {
var $elem = $(elem);
// First check if elem is hidden through css as this is not very costly:
if ($elem.getStyle('display') == 'none' || $elem.getStyle('visibility') == 'hidden' ) {
//elem is set through CSS stylesheet or inline to invisible
return false;
}
//Now check for the elem being outside of the viewport
var $elemOffset = $elem.viewportOffset();
if ($elemOffset.left < 0 || $elemOffset.top < 0) {
//elem is left of or above viewport
return false;
}
var vp = document.viewport.getDimensions();
if ($elemOffset.left > vp.width || $elemOffset.top > vp.height) {
//elem is below or right of vp
return false;
}
//Now check for elements positioned on top:
//TODO: Build check for this using Prototype...
//Neither of these was true, so the elem was visible:
return true;
}
/**
* Checks display and visibility of elements and it's parents
* #param DomElement el
* #param boolean isDeep Watch parents? Default is true
* #return {Boolean}
*
* #author Oleksandr Knyga <oleksandrknyga#gmail.com>
*/
function isVisible(el, isDeep) {
var elIsVisible = true;
if("undefined" === typeof isDeep) {
isDeep = true;
}
elIsVisible = elIsVisible && el.offsetWidth > 0 && el.offsetHeight > 0;
if(isDeep && elIsVisible) {
while('BODY' != el.tagName && elIsVisible) {
elIsVisible = elIsVisible && 'hidden' != window.getComputedStyle(el).visibility;
el = el.parentElement;
}
}
return elIsVisible;
}
You can use the clientHeight or clientWidth properties
function isViewable(element){
return (element.clientHeight > 0);
}
Prototype's Element library is one of the most powerful query libraries in terms of the methods. I recommend you to check out the API.
A few hints:
Checking visibility can be a pain, but you can use the Element.getStyle() method and Element.visible() methods combined into a custom function. With getStyle() you can check the actual computed style.
I don't know exactly what you mean by "underneath" :) If you meant by it has a specific ancestor, for example, a wrapper div, you can use Element.up(cssRule):
var child = $("myparagraph");
if(!child.up("mywrapper")){
// I lost my mom!
}
else {
// I found my mom!
}
If you want to check the siblings of the child element you can do that too:
var child = $("myparagraph");
if(!child.previous("mywrapper")){
// I lost my bro!
}
else {
// I found my bro!
}
Again, Element lib can help you if I understand correctly what you mean :) You can check the actual dimensions of the viewport and the offset of your element so you can calculate if your element is "off screen".
Good luck!
I pasted a test case for prototypejs at http://gist.github.com/117125. It seems in your case we simply cannot trust in getStyle() at all. For maximizing the reliability of the isMyElementReallyVisible function you should combine the following:
Checking the computed style (dojo has a nice implementation that you can borrow)
Checking the viewportoffset (prototype native method)
Checking the z-index for the "beneath" problem (under Internet Explorer it may be buggy)
One way to do it is:
isVisible(elm) {
while(elm.tagName != 'BODY') {
if(!$(elm).visible()) return false;
elm = elm.parentNode;
}
return true;
}
Credits: https://github.com/atetlaw/Really-Easy-Field-Validation/blob/master/validation.js#L178
Try element.getBoundingClientRect().
It will return an object with properties
bottom
top
right
left
width -- browser dependent
height -- browser dependent
Check that the width and height of the element's BoundingClientRect are not zero which is the value of hidden or non-visible elements. If the values are greater than zero the element should be visible in the body. Then check if the bottom property is less than screen.height which would imply that the element is withing the viewport. (Technically you would also have to account for the top of the browser window including the searchbar, buttons, etc.)
Catch mouse-drag and viewport events (onmouseup, onresize, onscroll).
When a drag ends do a comparison of the dragged item boundary with all "elements of interest" (ie, elements with class "dont_hide" or an array of ids). Do the same with window.onscroll and window.onresize. Mark any elements hidden with a special attribute or classname or simply perform whatever action you want then and there.
The hidden tests are pretty easy. For "totally hidden" you want to know if ALL corners are either inside the dragged-item boundary or outside the viewport. For partially hidden you're looking for a single corner matching the same test.
I don't think checking the element's own visibility and display properties is good enough for requirement #1, even if you use currentStyle/getComputedStyle. You also have to check the element's ancestors. If an ancestor is hidden, so is the element.
Check elements' offsetHeight property. If it is more than 0, it is visible. Note: this approach doesn't cover a situation when visibility:hidden style is set. But that style is something weird anyways.
Here is a sample script and test case. Covers positioned elements, visibilty: hidden, display: none. Didn't test z-index, assume it works.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title></title>
<style type="text/css">
div {
width: 200px;
border: 1px solid red;
}
p {
border: 2px solid green;
}
.r {
border: 1px solid #BB3333;
background: #EE9999;
position: relative;
top: -50px;
height: 2em;
}
.of {
overflow: hidden;
height: 2em;
word-wrap: none;
}
.of p {
width: 100%;
}
.of pre {
display: inline;
}
.iv {
visibility: hidden;
}
.dn {
display: none;
}
</style>
<script src="http://www.prototypejs.org/assets/2008/9/29/prototype-1.6.0.3.js"></script>
<script>
function isVisible(elem){
if (Element.getStyle(elem, 'visibility') == 'hidden' || Element.getStyle(elem, 'display') == 'none') {
return false;
}
var topx, topy, botx, boty;
var offset = Element.positionedOffset(elem);
topx = offset.left;
topy = offset.top;
botx = Element.getWidth(elem) + topx;
boty = Element.getHeight(elem) + topy;
var v = false;
for (var x = topx; x <= botx; x++) {
for(var y = topy; y <= boty; y++) {
if (document.elementFromPoint(x,y) == elem) {
// item is visible
v = true;
break;
}
}
if (v == true) {
break;
}
}
return v;
}
window.onload=function() {
var es = Element.descendants('body');
for (var i = 0; i < es.length; i++ ) {
if (!isVisible(es[i])) {
alert(es[i].tagName);
}
}
}
</script>
</head>
<body id='body'>
<div class="s"><p>This is text</p><p>More text</p></div>
<div class="r">This is relative</div>
<div class="of"><p>This is too wide...</p><pre>hidden</pre>
<div class="iv">This is invisible</div>
<div class="dn">This is display none</div>
</body>
</html>
Here is a part of the response that tells you if an element is in the viewport.
You may need to check if there is nothing on top of it using elementFromPoint, but it's a bit longer.
function isInViewport(element) {
var rect = element.getBoundingClientRect();
var windowHeight = window.innerHeight || document.documentElement.clientHeight;
var windowWidth = window.innerWidth || document.documentElement.clientWidth;
return rect.bottom > 0 && rect.top < windowHeight && rect.right > 0 && rect.left < windowWidth;
}

Changing div position to fixed with javascript attaches the div to document not viewport

I am looking for a way to determine if any part of a div is touching the top of the viewport and fix an item in that div to the top of the viewport using vanilla javascript.
I have been able to sort out how to determine if the div is touching the top of the viewport and trigger changes to the div's style. But for some reason when I change the div's position: absolute to position: fixed the div fixes to the top of the document, not to the top of the viewport, hence is not visible.
My js
function touchTop() {
var div = $('itin');
var rect = div.getBoundingClientRect();
var y = rect.top;
var h = rect.bottom;
if ((y < 0) && (h > 0)) {
document.getElementById('seemore').style.position = 'fixed';
document.getElementById('seemore').style.top = '45%';
} else {
document.getElementById('seemore').style.position = 'absolute';
document.getElementById('seemore').style.top = '66px';
}
}
window.addEventListener('scroll', touchTop);
The basic div HTML
<div id="itin" class="container">
<div class="sp20"></div>
<div class="text rgt">
<h3>your daily adventures</h3>
<p>blah blah blah</p>
</div>
<div id="seemore" class="ghstbtn">See More</div>
</div>
And the basic initial CSS
#seemore {
width: auto;
position: absolute;
top: 66px;
right: 20px;
}
To clarify further: My problem that needs solving is that when javascript changes the style.position to fixed the #seemore div gets positioned such that the 'top' value is measured from the top of the document, not from the top of the viewport. So basically not visible in the viewport.
If I understand you correctly, I think your problem lays here:
if ((y < 0) && (h > 0))
When the container div hits the top of the document, position to "seemore" is set to fixed, but as fast as the bottom of the div hits the top of the document,
"h === 0" and the position is again set to absolute.
Try this.
const seeMore = document.getElementById('seemore');
const div = document.getElementById('itin');
window.addEventListener('scroll', checkBoundries);
function checkBoundries() {
var rect = div.getBoundingClientRect();
var y = rect.top;
var h = rect.bottom;
if (y < 0) {
seeMore.style.position = 'fixed';
seeMore.style.top = '45%';
} else {
seeMore.style.position = 'absolute';
seeMore.style.top = '66px';
}
}
Turns out to be related to a filter applied to a parent of a parent of a parent of a ....
Found some old questions regarding issues with transformations and position:fixed dating back some 5 to 7 years ago. And even though many mentioned filing issue reports with the browser makers, it seems the problem has never been addressed and resolved. One comment mentioned filters could also cause the issue.
Moved the filter to a separate class which is now added and removed, rather than applying the filter directly to the div. And everything works as expected.

Keep a div visible when content would push it down

I want to have a div positioned in the bottom of another div. This i can solve with just
bottom: 0px;
postion: fixed;
But, if the containing div is larger than the window, i want to freeze the inner div to the bottom of the window.
If it's easier the first condition can be scrapped and the inner div can just be positioned under the content, the important part is that the content must always be visible.
The best solution would be to detect with JavaScript if the footer is visible inside the viewport. If not, you should change it's styles to stick to the bottom of the window instead of that of the containing div.
You could use this function to see if it's in the viewport:
function elementInViewport(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;
while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}
return (
top >= window.pageYOffset &&
left >= window.pageXOffset &&
(top + height) <= (window.pageYOffset + window.innerHeight) &&
(left + width) <= (window.pageXOffset + window.innerWidth)
);
}
(taken from How to tell if a DOM element is visible in the current viewport?)
Now, every time you scroll or resize the page you can do a check that runs that function. Based on that, you can decide to set a class or change a CSS property that will do what you're looking for.
Since you didn't include any code (in the future, please do) I'm going to assume your code looks something like this:
<div class="wrapper">
(contents)
<div class="footer">footer</div>
</div>
To stick the .footer to the bottom of .wrapper, it has to have a 'positon: absolute' and the wrapper will need a position: relative. However, if you change it's position property to fixed and the wrapper to static (the default for all elements), the footer is going to stick to the bottom of the window instead.
View this example http://jsfiddle.net/GMYEh/
Now, using the script above you can tell which of the two it should be. You have to use a fake element at the same position of the footer, instead of the footer itself. That way, if you move the footer to the bottom of the window, you can still measure whether or not the bottom of the wrapper is in the viewport. (If you measure the footer itself and move it you'll get stuck).
The script that does this (in jQuery):
// add a fake footer after the wrapper
$('.wrapper').after($('<div class="fakefooter" />'));
$(document).on('resize scroll', function(e){
//measure if the fake footer is in viewport
if(elementInViewport($('.fakefooter')[0])) {
// If so, it should be in the bottom of the wrapper.
$('.wrapper').css('position', 'relative');
$('.footer').css('position', 'absolute');
} else {
// else it should be in the bottom of the window
$('.wrapper').css('position', 'static');
$('.footer').css('position', 'fixed');
}
});
Working example:
http://jsfiddle.net/GMYEh/4/
Try this:
HTML:
<div id="wrapper">
<div id="innerContent"></div>
</div>
CSS:
.fixedContent {
position: fixed;
bottom: 0;
}
and the javascript:
var wrapper = document.getElementById('wrapper');
var content = document.getElementById('innerContent');
function position() {
if (wrapper.offsetHeight + wrapper.offsetTop - content.offsetHeight - window.scrollY > window.innerHeight) {
content.className += ' fixedContent';
} else {
content.className = content.className.replace('fixedContent', '');
}
}
window.onload = position;
window.onresize = position;
If you're open to jQuery you can make the javascript more simple and compatible
var $wrapper = $('#wrapper');
var $content = $('#innerContent');
$(window).on('load resize', function() {
$content.toggleClass('fixedContent', $wrapper.outerHeight(true) $content.offset().top - $content.outerHeight(true) - $(document).scrollTop() > $(window).height());
});
EDIT:
I modified the conditions a bit adding the vertical scroll value and top offset.

how can I determine that an element height is fixed

I need to know whether an HTML element will expand as the content is added to it. The height of the element can be preset in a number of ways - with the inline style height or max-height, by setting relative height when the parent element has its height set, via a css class, etc.
All I need to know whether the height of the element will increase as I add children. I hoped to use the JQuery css method, but it computes the actual value and does not tell me whether it will change as new children are added.
There are several ways to set a height of the element. Here is a test function you can use in order to determine if specified element has fixed height:
HTML
<span id="notAffected"></span>
<div id="hasCSSHeight"/>
<div id="hasInlineHeight" style="height:50px"/>
<div id="hasAttribureHeight" height="10px" />
<div id="hasNoHeight"/>
CSS
#notAffected,
#hasCSSHeight {
height: 100px;
}
JavaScript
function hasHeight(el) {
var i,
l,
rules = window.getMatchedCSSRules(el),
display = window.getComputedStyle(el).getPropertyValue('display');
// Inline displayed elements are not affected by height definition
if(display === 'inline') {
return false;
}
// CSS
if(rules) {
for(i=0,l=rules.length;i<l;i++) {
if(rules[i].style.getPropertyValue('height')) {
return true;
}
}
}
// Inline style
if(el.style.height) {
return true;
}
// Attribute
if(el.getAttribute('height')) {
return true;
}
return false;
}
Example see here http://jsbin.com/usojec/3/edit

Can't get left position with global stylesheet in JavaScript

I need to get the upper left position of an image in JavaScript. I define the location of an image in a global style sheet:
<style type="text/css">
img.movable { position:relative; top:0px; left:375px; }
</style>
When I define the image using the global style
<img id="image11" class="movable" src="testimage.jpg" onclick="jump()" />
The style.left attribute is empty:
<script type="text/javascript">
function jump() {
xpos = document.getElementById("image11").style.left;
alert( "style.left="+xpos );
xpos = document.getElementById("image11").offsetLeft;
alert( "offsetLeft="+xpos );
}
</script>
But when I define the image using an inline style:
<img id="image11" style="position:relative; top:0px; left:375px;" src="logo.jpg" onclick="jump()" />
style.left contains a value.
This behaviour is the same for IE8 and Firefox. Any ideas why that is?
Cheers,
Martin.
el.style is actually a map of all the css properties applied using the style attribute. To get styles defined in stylesheets or default browser styles, you need to use computed style. Quirksmode, as usual, is a first stop.
The DOM element that represents the IMG tag you specified has its own style property, and its values will override any given by globals. Unfortunately in this case, the property of the element you specified will indeed be empty, as the values do not cascade down to the JavaScript object level.
You will need to get the position from either the offsetLeft property or the CSS rule itself.
.style properties only relate to inline css. Styles specified in a stylesheet will thus not be present in the .style list. If you want to get the position of an element on the page (as opposed to viewport) something like this should work:
function getNodePosition(node) {
var top = left = 0;
while (node) {
if (node.tagName) {
top = top + node.offsetTop;
left = left + node.offsetLeft;
node = node.offsetParent;
} else {
node = node.parentNode;
}
}
return [top, left];
}
Adapted from Quirksmode. OffsetTop and OffsetLeft are wrt to the parent, so this iterates up the tree to get the total offsets, and hence page position. If you want to know the position wrt the viewport, you can adjust the values returned here by the scroll distance.
Get Left Element of a given HTMLElement
function getHTMLElementLeft(HTMLElement, left)
{
if (left == undefined)
{
var left = 0;
}
if (HTMLElement.nodeType == 1)
{
left += HTMLElement.offsetLeft;
if (HTMLElement.offsetParent)
{
left = getHTMLElementLeft(HTMLElement.offsetParent, left);
}
}
return left;
}

Categories

Resources