In codepen, Intersection observer has different behavior from browser window - javascript

If I write this codes in separate HTML, CSS and Javascript files and open it with a browser, sticky sharebar appears when the target observed in middle of viewport height, but in codepen appears when the target observed in bottom of viewport height. What is the reason?
{
class StickyShareBar {
constructor(element) {
this.element = element;
this.contentTarget = document.getElementsByClassName('js-sticky-sharebar-target');
this.showClass = 'sticky-sharebar--on-target';
this.threshold = '50%';
this.initShareBar();
}
initShareBar() {
if(this.contentTarget.length < 1) {
this.element.addClass( this.showClass);
return;
}
if(intersectionObserverSupported) {
this.initObserver();
} else {
this.element.addClass(this.showClass);
}
}
initObserver() {
const self = this;
var observer = new IntersectionObserver(
function(entries, observer) {
self.element.classList.toggle( self.showClass, entries[0].isIntersecting);
},
{
rootMargin: "0px 0px -"+this.threshold+" 0px"}
);
observer.observe(this.contentTarget[0]);
}
}
const stickyShareBar = document.getElementsByClassName('js-sticky-sharebar'),
intersectionObserverSupported = ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype);
new StickyShareBar(stickyShareBar[0]);
}

It might be a problem with rootMargin and the fact that you are using an iframe
https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin
https://github.com/w3c/IntersectionObserver/issues/372

It's because of the targeted element. When the srcollbar can reach the targeted element ( js-sticky-sharebar-target ) then the event is fired. When the content container width is smaller the scroll wheel can't reach the targeted element. For this reason it's not showing on browser or small screens. I have changed the targeted element and placed it on the top. Now it's working as you expected.
Changed HTML:
<div class="container new-js-sticky-sharebar-target">
Changed JS:
this.contentTarget = document.getElementsByClassName('new-js-sticky-sharebar-target');
See Demo

The IntersectionObserver spec has been expanded to allow passing a Document as an argument to root. So if you pass the Document of the iframe as the argument for root it triggers a special case where it will consider the iframe's window as the viewport and hence it will work as expected. In something like codepen you probably have no control over this but outside of that it will fix your problem.
See https://github.com/w3c/IntersectionObserver/issues/372

I've had this exact same issue many times when using Intersection Observer in CodePen. Like others have said, it's because CodePen renders your work in an iframe and the rootMargin doesn't work the way you might expect because of that.
I have tried pretty much every solution that has been described in other threads and this is the only one I have gotten to work: https://codepen.io/nickcil/pen/MWbqOaJ
The solution is to wrap your HTML in a full width and height element that you set to position: fixed and overflow: auto. Then set that element as the root for your observer. rootMargin will now work as expected in your pen.

Related

Can I use IntersectionObserver to detect where an element is with respect to bottom of window?

Right now I manage lazy loading of images when an element at the bottom of the screen becomes visible, like so:
let options = { rootMargin: '0px', threshold: 0.1 }
let callback = (entries, observer) => {
console.log(entries[0]);
var bar = entries[0];
if (entries.filter(entry => entry.isIntersecting).length) {
// load images
}
};
let observer = new IntersectionObserver(callback, options);
observer.observe(document.querySelector('#bottombar'))
This however means that the new images get loaded only when the bottom is reached, which gives a hiccup-y feel to the interaction.
Is it possible to fire the event when - for example - the element is a full screen height (or less) below the bottom of the screen? I guess it could be done with scroll events - but IntersectionObserver seems cleaner (and fires less often).
Yes you can.
Check this out: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
You can create an IntersectionObserver and provide a custom root property which corresponds to an element bigger than the viewport (e.g. height: 200vh).
Also: if you provide a threshold of 0 you shouldn't have the hiccups you describe.
Another thing you can do is set a rootMargin of 100% to indicate that the margin is a whole new viewport.

An event or observer for changes to getBoundingClientRect()

Is there a way to detect when an element's getBoundingClientRect() rectangle has changed without actually calculating getBoundingClientRect()? Something like a "dirty flag"? Naively, I assume that there must be such a mechanism somewhere in the internal workings of browsers, but I haven't been able to find this thing exposed in the DOM API. Maybe there is a way to do this with MutationObservers?
My application is a web component that turns DOM elements into nodes of a graph, and draws the edges onto a full screen canvas. See here.
Right now, I'm calling getBoundingClientRect() for every element, one time per animation frame frame, even when nothing is changing. It's feeling expensive. I'm usually getting %15-%50 CPU usage on a decently powerful computer at 60 fps.
Does anyone know of such a thing? Do you think it's reasonable to expect something like this? Is this kind of thing feasible? Has it ever been proposed before?
As mentioned in the comments above. The APIs you're looking for are: ResizeObserver and IntersectionObserver. However, there are a few things to note:
ResizeObserver will only fire when the observed element changes size. And it will essentially only give you correct values for width and height.
Both ResizeObserver and IntersectionObserver are supposed to not block paint
ResizeObserver will trigger after layout but before paint, which essentially makes it feel synchronous.
IntersectionObserver fires asynchronously.
What if you need position change tracking
This is what IntersectionObserver is made for. It can often be used for visibility detection. The problem here is that IntersectionObserver only fires when the ratio of intersection changes. This means that if a small child moves around within a larger container div, and you track intersection between the parent and the child, you won't get any events except when the child is entering or exiting the parent.
You can still track when an element moves at all. This is how:
Start by measuring the position of the element you want to track using getBoundingClientRect.
Insert a div as an absolutely positioned direct child of body which is positioned exactly where the tracked element is.
Start tracking the intersection between this div and the original element.
The intersection should start at 1. Whenever it changes to something else:
Remeasure the element using getBoundingClientRect.
Fire the position/size changed event
update the styles of the custom div to the new position of the element.
the observer should fire again with the intersection ratio at 1 again, this value can be ignored.
NOTE: this technique can also be used for more efficient polypill for ResizeObserver which is a newer feature than IntersectionObserver. The commonly available polyfills rely on MutationObserver which is considerably less efficient.
I have had mediocre success with a combination of 3 observers:
An event listener listens for scroll events on any of the scrollable ancestors of the observed element. For this, a capture scroll event listener can be registered on the document, which checks if the event target is an ancestor of the observed element.
A ResizeObserver detects resizes of the observed element.
An IntersectionObserver detects position changes of the observed element. To achieve this, an invisible child element is added to the observed element, 2×2 pixels in size, positioned using position: fixed. The invisible element does not have any top or left coordinates, causing it to be rendered inside the observed element, but it is moved using a negative margin-left and margin-top to an absolute position of -1,-1 in the top left corner of the viewport. With this positioning, 1 of the 4 pixels is visible in the viewport (at position 0,0 of the viewport), while the other 3 pixels are invisible (at position -1,-1, -1,0 and 0,-1 of the viewport). As soon as the observed element moves, its invisible child moves with it, causing 0 or more than 1 pixel to be visible and the IntersectionObserver to fire, leading us to emit a change event and repositioning the invisible element.
function observeScroll(element: HTMLElement, callback: () => void): () => void {
const listener = (e: Event) => {
if ((e.target as HTMLElement).contains(element)) {
callback();
}
};
document.addEventListener('scroll', listener, { capture: true });
return () => {
document.removeEventListener('scroll', listener, { capture: true });
};
}
function observeSize(element: HTMLElement, callback: () => void): () => void {
const resizeObserver = new ResizeObserver(() => {
callback();
});
resizeObserver.observe(element);
return () => {
resizeObserver.disconnect();
};
}
function observePosition(element: HTMLElement, callback: () => void): () => void {
const positionObserver = document.createElement('div');
Object.assign(positionObserver.style, {
position: 'fixed',
pointerEvents: 'none',
width: '2px',
height: '2px'
});
element.appendChild(positionObserver);
const reposition = () => {
const rect = positionObserver.getBoundingClientRect();
Object.assign(positionObserver.style, {
marginLeft: `${parseFloat(positionObserver.style.marginLeft || '0') - rect.left - 1}px`,
marginTop: `${parseFloat(positionObserver.style.marginTop || '0') - rect.top - 1}px`
});
};
reposition();
const intersectionObserver = new IntersectionObserver((entries) => {
const visiblePixels = Math.round(entries[0].intersectionRatio * 4);
if (visiblePixels !== 1) {
reposition();
callback();
}
}, {
threshold: [0.125, 0.375, 0.625, 0.875]
});
intersectionObserver.observe(positionObserver);
return () => {
intersectionObserver.disconnect();
positionObserver.remove();
};
}
export function observeBounds(element: HTMLElement, callback: () => void): () => void {
const destroyScroll = observeScroll(element, callback);
const destroySize = observeSize(element, callback);
const destroyPosition = observePosition(element, callback);
return () => {
destroyScroll();
destroySize();
destroyPosition();
};
}
When using this code, keep in mind that the callback is called synchronously and will block whatever event is calling it. The callback should call whatever actions asynchronously (for example using setTimeout()).
Note: There are some situations where this will not work:
When the observed element has an ancestor that acts as a containing block for fixed elements (transform, perspective, filter or will-change: transform is set, see MDN under fixed) AND has overflow set to something else than visible, fixed descendants will not be able to escape the containing block. This will cause permanent invisibility of all 4 pixels of the position detector element, so the IntersectionObserver will not fire on position changes. I am looking for a solution here.
When the observer is used inside an iframe and the top left corner of the iframe is out of view, the IntersectionObserver will also report 0 pixels of visibility.

TinyMce submenu's not sticking to toolbar when using fixed_toolbar_container and absolute position

We would like to have a greater control of where and how we position the tinymce toolbar. We found this option fixed_toolbar_container which solves a lot for us but brings us an anoying problem. The documents say the fixed_toolbar_container (http://www.tinymce.com/wiki.php/Configuration:fixed_toolbar_container) can be used to have a fixed toolbar. But we actually would like to use it to be absolute so we can position it relative to it's container.
I created a JS Fiddle to demonstrate the problem: http://jsfiddle.net/ronfmLym/2/. When you open the toolbar by clicking on the text the toolbar will be positioned absolute. When you open a submenu (i.e. by clicking on "file") a submenu will open. Now when you start scrolling the submenu won't stick to the toolbar. This is because these submenu's get the mce-fixed class because we set the fixed_toolbar_container property.
<div class="element">
<div class="toolbar-container"></div>
<div class="content">
<p>Text</p>
</div>
</div>
Is there any way to make the submenu's stick to the toolbar when positioned absolute and scrolling? Keep in mind that we are switching to a fixed positioning when the toolbar is going off screen.
We thought we could maybe fix it by modifying the container element of de submenu's by using the piece of code below and overwriting the top-position of the submenu's and setting the positioner to absolute with css. But that seems to mess up the tooltips and tinymce doesn't recalculate the "left" css-property of the submenu's so the position in still off.
tinymce.ui.Control.prototype.getContainerElm = function() {
return document.getElementById('toolbar-container');
};
The only corresponding question I could find on stackoverflow was this one: TinyMCE push down submenus using fixed_toolbar_container, no answers there.
Tried wrapping the toolbar in a div and using position:relative; to try and hack it together, but didn't cooperate this time.
It appears that the toolbar actually is accounting for its position at the time of click. So your only conflict is if the opened toolbar is position:absolute and then changes to position:fixed or vice versa.
Your best [manual] bet would be to call a function at the same time that you change the position of the toolbar that:
Detects if any menus are open.
Changes the toolbar position.
Reopens the menus that were open.
The lazy (discouraged) fix would be to close all submenus whenever the position changes. This will fix the layout, but it will require the user to click once again to bring the menu back.
Sorry this isn't a silver bullet answer :(
This answer follows Brian John's suggestion:
I'm using this method to position any open mce-floatpanel (This is typescript, but it shouldn't be too hard to convert to ES or whatever you need.):
positionTinyMceDropdowns() {
// TODO: You'll need to replace all occurrences
// of this.mceWrapperElement with whatever is
// wrapping your TinyMCE. If you have only a
// single instance, you can just replace it
// with document
const button = <HTMLElement> this.mceWrapperElement.getElementsByClassName('mce-opened').item(0);
const items = document.getElementsByClassName('mce-floatpanel');
let wrapperNode: HTMLElement;
for (let i = 0; i < items.length; i++) {
const currentItem = <HTMLElement> items.item(i);
if (currentItem.style.display !== 'none') {
wrapperNode = currentItem;
break;
}
}
if (!wrapperNode || !button) {
return;
}
const styles = wrapperNode.style;
styles.display = 'block';
styles.position = 'absolute';
const bodyRect = document.body.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
// get absolute button position:
let y = buttonRect.top - bodyRect.top;
y += 33; // toolbar line height;
styles.top = `${Math.floor(y)}px`;
}
The instances I found in which it needs to be called:
on window scroll (or if the editor is wrapped in a scrolling container, then whenever that scrolls)
on window resize (or if the editor is wrapped in a container that resizes without the window being resized, then whenever that container is resized)
So here's a sample for the simplest case in angular (again, adept to whichever js framework you're using):
import { HostListener } from '#angular/core';
// ...
#HostListener('window:resize', ['$event'])
#HostListener('window:scroll', ['$event'])
public onResize() {
this.positionTinyMceDropdowns();
}
Interestingly on iOS devices (and perhaps other mobile devices?) mce-floatpanel wasn't even positioned correctly after it had just been opened. So I had to add this:
tinymceConfig.setup = (editor: TinyMceEditor) => {
editor.on('init', () => {
const panel = this.mceWrapperElement.querySelector('.mce-tinymce.mce-panel');
if (panel) {
panel.addEventListener('touchend', () => {
this.positionTinyMceDropdowns();
});
}
});
};
I think the config setting fixed_toolbar_container is poorly explained in TinyMCE 6 documentation but when you correctly configure it, you'll find it will work much more nice (especially for inline mode) than the default configuration that tries to emulate position:sticky.
In practice, you want to fixed_toolbar_container set to a string that's CSS selector for the container, typically something like "#mycontainer".
After that, you can move the container element using its CSS properties, the TinyMCE user interface nicely follows around. (Modulo typical TinyMCE bugs, of course. For example, the submenus overflow to right with very narrow viewports.)
Note that TinyMCE positions stuff where it uses position:absolute relative to fixed_toolbar_container container and if you move that container around, in some cases you must execute editor.dispatch("ResizeWindow") to trigger TinyMCE to re-calculate the absolutely positioned elements.
See demo using custom container with position:sticky at https://jsfiddle.net/8bndv26t/1/.

Google Translate Bar moving content down

Is there a way to stop Google Translate Bar from moving my content down? I have a static background image, and a header image that corresponds with the background image, so when the Google Translate Bar is fixed to the top of my screen, it moves my top content down and out of the background image.
Is there a way to make it just statically over my content or fixed in such a way it won't move my content down?
Or Can I detect is Translation is taking place, then move my background accordingly? I tried to use this but it doesn't revert back if I remove the Translation Bar:
document.addEventListener('DOMSubtreeModified', function (e) {
if(e.target.tagName === 'HTML' && window.google) {
if(e.target.className.match('translated')) {
document.body.style.backgroundPosition="0px 40px";
} else {
document.body.style.backgroundPosition="0px 0px";
}
}
}, true);
It's a bit difficult without a code example, but the easiest solution would be to set position: fixed; and top: 0 on the translate bar, however, this means it will always remain at the top of the page once you scroll down.
If the translate bar is near the top of your document, which it sounds like it is, you can set the position to absolute instead, keeping the top: 0 declaration. This should make it appear at the top of the closest positioned ancestor, i.e. an element with position set to relative, absolute, fixed, or sticky. If this doesn't exist, it'll be positioned according to the root tag, i.e. <html> in a well-formed document. Here, you could set position: relative on your <body>, for example.
Both fixed and sticky takes the element entirely out of the document flow, so they will do exactly what you're requesting here: appear on top of other content.
The addEventListener('DOMSubtreeModified') is outdated. What you want is to use DOM MutationObserver Events to apply the change. This DOM API is available on all major browser since 2012 I think.
I use this on to lower the google translator bar, so maybe moving the bar down also solves your problem. If not, just change the callback function and variables for your need.
The google translator creates an iframe element like this:
<iframe id=":1.container" class="goog-te-banner-frame skiptranslate" frameborder="0" src="javascript:''" style="visibility: visible; top: calc(100% - 40px);">...</iframe>
So the MutationObserver code to move that element down is as follow:
//Observer for Google translator bar creation and action to move to bottom
// Select the nodetree that will be observed for mutations
var nodetree = document.getElementsByTagName("body")[0];
// Select the target node atributes (CSS selector)
var targetNode = "iframe.goog-te-banner-frame";
// Options for the observer (which mutations to observe)
var config = { attributes: false, childList: true };
// Callback function to execute when mutations of DOM tree are observed
var lowerGoogleTranslateBar = function(mutations_on_DOMtree) {
for(var mutation of mutations_on_DOMtree) {
if (mutation.type == 'childList') {
console.log(mutation);
if (document.querySelector(targetNode) != null) {
//40px is the height of the bar
document.querySelector(targetNode).style.setProperty("top", "calc(100% - 40px)");
//after action is done, disconnect the observer from the nodetree
observerGoogleTranslator.disconnect();
}
}
}
};
// Create an observer instance linked to the callback function
var observerGoogleTranslator = new MutationObserver(lowerGoogleTranslateBar);
// Start observing the target node for configured mutations
observerGoogleTranslator.observe(nodetree, config);
You can learn more about this here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

jQuery: Get height of hidden element in jQuery

I need to get height of an element that is within a div that is hidden. Right now I show the div, get the height, and hide the parent div. This seems a bit silly. Is there a better way?
I'm using jQuery 1.4.2:
$select.show();
optionHeight = $firstOption.height(); //we can only get height if its visible
$select.hide();
You could do something like this, a bit hacky though, forget position if it's already absolute:
var previousCss = $("#myDiv").attr("style");
$("#myDiv").css({
position: 'absolute', // Optional if #myDiv is already absolute
visibility: 'hidden',
display: 'block'
});
optionHeight = $("#myDiv").height();
$("#myDiv").attr("style", previousCss ? previousCss : "");
I ran into the same problem with getting hidden element width, so I wrote this plugin call jQuery Actual to fix it. Instead of using
$('#some-element').height();
use
$('#some-element').actual('height');
will give you the right value for hidden element or element has a hidden parent.
Full documentation please see here. There is also a demo include in the page.
Hope this help :)
You are confuising two CSS styles, the display style and the visibility style.
If the element is hidden by setting the visibility css style, then you should be able to get the height regardless of whether or not the element is visible or not as the element still takes space on the page.
If the element is hidden by changing the display css style to "none", then the element doesn't take space on the page, and you will have to give it a display style which will cause the element to render in some space, at which point, you can get the height.
I've actually resorted to a bit of trickery to deal with this at times. I developed a jQuery scrollbar widget where I encountered the problem that I don't know ahead of time if the scrollable content is a part of a hidden piece of markup or not. Here's what I did:
// try to grab the height of the elem
if (this.element.height() > 0) {
var scroller_height = this.element.height();
var scroller_width = this.element.width();
// if height is zero, then we're dealing with a hidden element
} else {
var copied_elem = this.element.clone()
.attr("id", false)
.css({visibility:"hidden", display:"block",
position:"absolute"});
$("body").append(copied_elem);
var scroller_height = copied_elem.height();
var scroller_width = copied_elem.width();
copied_elem.remove();
}
This works for the most part, but there's an obvious problem that can potentially come up. If the content you are cloning is styled with CSS that includes references to parent markup in their rules, the cloned content will not contain the appropriate styling, and will likely have slightly different measurements. To get around this, you can make sure that the markup you are cloning has CSS rules applied to it that do not include references to parent markup.
Also, this didn't come up for me with my scroller widget, but to get the appropriate height of the cloned element, you'll need to set the width to the same width of the parent element. In my case, a CSS width was always applied to the actual element, so I didn't have to worry about this, however, if the element doesn't have a width applied to it, you may need to do some kind of recursive traversal of the element's DOM ancestry to find the appropriate parent element's width.
Building further on user Nick's answer and user hitautodestruct's plugin on JSBin, I've created a similar jQuery plugin which retrieves both width and height and returns an object containing these values.
It can be found here:
http://jsbin.com/ikogez/3/
Update
I've completely redesigned this tiny little plugin as it turned out that the previous version (mentioned above) wasn't really usable in real life environments where a lot of DOM manipulation was happening.
This is working perfectly:
/**
* getSize plugin
* This plugin can be used to get the width and height from hidden elements in the DOM.
* It can be used on a jQuery element and will retun an object containing the width
* and height of that element.
*
* Discussed at StackOverflow:
* http://stackoverflow.com/a/8839261/1146033
*
* #author Robin van Baalen <robin#neverwoods.com>
* #version 1.1
*
* CHANGELOG
* 1.0 - Initial release
* 1.1 - Completely revamped internal logic to be compatible with javascript-intense environments
*
* #return {object} The returned object is a native javascript object
* (not jQuery, and therefore not chainable!!) that
* contains the width and height of the given element.
*/
$.fn.getSize = function() {
var $wrap = $("<div />").appendTo($("body"));
$wrap.css({
"position": "absolute !important",
"visibility": "hidden !important",
"display": "block !important"
});
$clone = $(this).clone().appendTo($wrap);
sizes = {
"width": $clone.width(),
"height": $clone.height()
};
$wrap.remove();
return sizes;
};
Building further on Nick's answer:
$("#myDiv").css({'position':'absolute','visibility':'hidden', 'display':'block'});
optionHeight = $("#myDiv").height();
$("#myDiv").css({'position':'static','visibility':'visible', 'display':'none'});
I found it's better to do this:
$("#myDiv").css({'position':'absolute','visibility':'hidden', 'display':'block'});
optionHeight = $("#myDiv").height();
$("#myDiv").removeAttr('style');
Setting CSS attributes will insert them inline, which will overwrite any other attributes you have in your CSS file. By removing the style attribute on the HTML element, everything is back to normal and still hidden, since it was hidden in the first place.
You could also position the hidden div off the screen with a negative margin rather than using display:none, much like a the text indent image replacement technique.
eg.
position:absolute;
left: -2000px;
top: 0;
This way the height() is still available.
I try to find working function for hidden element but I realize that CSS is much complex than everyone think. There are a lot of new layout techniques in CSS3 that might not work for all previous answers like flexible box, grid, column or even element inside complex parent element.
flexibox example
I think the only sustainable & simple solution is real-time rendering. At that time, browser should give you that correct element size.
Sadly, JavaScript does not provide any direct event to notify when element is showed or hidden. However, I create some function based on DOM Attribute Modified API that will execute callback function when visibility of element is changed.
$('[selector]').onVisibleChanged(function(e, isVisible)
{
var realWidth = $('[selector]').width();
var realHeight = $('[selector]').height();
// render or adjust something
});
For more information, Please visit at my project GitHub.
https://github.com/Soul-Master/visible.event.js
demo: http://jsbin.com/ETiGIre/7
Following Nick Craver's solution, setting the element's visibility allows it to get accurate dimensions. I've used this solution very very often. However, having to reset the styles manually, I've come to find this cumbersome, given that modifying the element's initial positioning/display in my css through development, I often forget to update the related javascript code. The following code doesn't reset the styles per say, but removes the inline styles added by javascript:
$("#myDiv")
.css({
position: 'absolute',
visibility: 'hidden',
display: 'block'
});
optionHeight = $("#myDiv").height();
optionWidth = $("#myDiv").width();
$("#myDiv").attr('style', '');
The only assumption here is that there can't be other inline styles or else they will be removed aswell. The benefit here, however, is that the element's styles are returned to what they were in the css stylesheet. As a consequence, you can write this up as a function where an element is passed through, and a height or width is returned.
Another issue I've found of setting the styles inline via js is that when dealing with transitions through css3, you become forced to adapt your style rules' weights to be stronger than an inline style, which can be frustrating sometimes.
By definition, an element only has height if it's visible.
Just curious: why do you need the height of a hidden element?
One alternative is to effectively hide an element by putting it behind (using z-index) an overlay of some kind).
In my circumstance I also had a hidden element stopping me from getting the height value, but it wasn't the element itself but rather one of it's parents... so I just put in a check for one of my plugins to see if it's hidden, else find the closest hidden element. Here's an example:
var $content = $('.content'),
contentHeight = $content.height(),
contentWidth = $content.width(),
$closestHidden,
styleAttrValue,
limit = 20; //failsafe
if (!contentHeight) {
$closestHidden = $content;
//if the main element itself isn't hidden then roll through the parents
if ($closestHidden.css('display') !== 'none') {
while ($closestHidden.css('display') !== 'none' && $closestHidden.size() && limit) {
$closestHidden = $closestHidden.parent().closest(':hidden');
limit--;
}
}
styleAttrValue = $closestHidden.attr('style');
$closestHidden.css({
position: 'absolute',
visibility: 'hidden',
display: 'block'
});
contentHeight = $content.height();
contentWidth = $content.width();
if (styleAttrValue) {
$closestHidden.attr('style',styleAttrValue);
} else {
$closestHidden.removeAttr('style');
}
}
In fact, this is an amalgamation of Nick, Gregory and Eyelidlessness's responses to give you the use of Gregory's improved method, but utilises both methods in case there is supposed to be something in the style attribute that you want to put back, and looks for a parent element.
My only gripe with my solution is that the loop through the parents isn't entirely efficient.
One workaround is to create a parent div outside the element you want to get the height of, apply a height of '0' and hide any overflow. Next, take the height of the child element and remove the overflow property of the parent.
var height = $("#child").height();
// Do something here
$("#parent").append(height).removeClass("overflow-y-hidden");
.overflow-y-hidden {
height: 0px;
overflow-y: hidden;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="parent" class="overflow-y-hidden">
<div id="child">
This is some content I would like to get the height of!
</div>
</div>
Here's a script I wrote to handle all of jQuery's dimension methods for hidden elements, even descendants of hidden parents. Note that, of course, there's a performance hit using this.
// Correctly calculate dimensions of hidden elements
(function($) {
var originals = {},
keys = [
'width',
'height',
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'offset',
'scrollTop',
'scrollLeft'
],
isVisible = function(el) {
el = $(el);
el.data('hidden', []);
var visible = true,
parents = el.parents(),
hiddenData = el.data('hidden');
if(!el.is(':visible')) {
visible = false;
hiddenData[hiddenData.length] = el;
}
parents.each(function(i, parent) {
parent = $(parent);
if(!parent.is(':visible')) {
visible = false;
hiddenData[hiddenData.length] = parent;
}
});
return visible;
};
$.each(keys, function(i, dimension) {
originals[dimension] = $.fn[dimension];
$.fn[dimension] = function(size) {
var el = $(this[0]);
if(
(
size !== undefined &&
!(
(dimension == 'outerHeight' ||
dimension == 'outerWidth') &&
(size === true || size === false)
)
) ||
isVisible(el)
) {
return originals[dimension].call(this, size);
}
var hiddenData = el.data('hidden'),
topHidden = hiddenData[hiddenData.length - 1],
topHiddenClone = topHidden.clone(true),
topHiddenDescendants = topHidden.find('*').andSelf(),
topHiddenCloneDescendants = topHiddenClone.find('*').andSelf(),
elIndex = topHiddenDescendants.index(el[0]),
clone = topHiddenCloneDescendants[elIndex],
ret;
$.each(hiddenData, function(i, hidden) {
var index = topHiddenDescendants.index(hidden);
$(topHiddenCloneDescendants[index]).show();
});
topHidden.before(topHiddenClone);
if(dimension == 'outerHeight' || dimension == 'outerWidth') {
ret = $(clone)[dimension](size ? true : false);
} else {
ret = $(clone)[dimension]();
}
topHiddenClone.remove();
return ret;
};
});
})(jQuery);
If you've already displayed the element on the page previously, you can simply take the height directly from the DOM element (reachable in jQuery with .get(0)), since it is set even when the element is hidden:
$('.hidden-element').get(0).height;
same for the width:
$('.hidden-element').get(0).width;
(thanks to Skeets O'Reilly for correction)

Categories