How to detect if element has 'auto' height - javascript

Both window.getComputedStyle(element).height and element.clientHeight are returning the current height of the element in pixels, regardless of the value set in the CSS.
Is there any way to find out if the height was set to auto, or other units than pixels ?
One solution that #pvnarula suggests through the page he linked is to temporarily change the contents of the element, then compare heights.
A little bit hacky...

Please try:
document.getElementById("ele_id").style.height
Also check the following plugin:
http://gregpettit.ca/2012/jquery-check-if-element-has-auto-widthheight/

Update:
Based on other answers and lot of online research I came up with a mix of everything in a single function. Check out the jsfiddle here:
https://jsfiddle.net/oriadam/d01ap7r6/3/
// input a jQuery element
// return true for elements with auto height (90-100% is considered auto as well)
// return false for elements with fixed height
function is_height_auto($e) {
var e = $e[0],
// check fixed style:
chk = function(value) {
return /\d/.test(value) && !/^(100|9\d)\%/.test(value);
};
// start from the first, easiest, inline styles
if (chk(e.style.height)) {
// console.log('fixed for having style', e.style.height)
return false;
}
// start from the first, easiest, inline styles
var overflow = getComputedStyle(e)['overflow'];
if (overflow == 'scroll' || overflow == 'auto' || (e.tagName == 'BODY' && overflow == 'visible')) {
// console.log('auto for having overflow or is body', getComputedStyle(e)['overflow'], e.tagName);
return true;
}
// deprecated chrome way - check each rule that applies to the element
if (typeof getMatchedCSSRules == 'function') {
var i, MatchedCSSRules = getMatchedCSSRules(e) || [];
for (i = MatchedCSSRules.length; i; i--) {
if (MatchedCSSRules[i - 1].style.height) {
// console.log('found height at MatchedCSSRules[' + (i - 1) + ']: ', MatchedCSSRules[i - 1], ' All matches: ', MatchedCSSRules)
return !chk(MatchedCSSRules[i - 1].style.height);
}
}
}
// append something, see if height was changed, remove the something
var originalHeight = $e.height(),
$ghost = jQuery('<b style="display:block;height:1px;width:1px;padding:0;margin:0;">').appendTo($e),
newHeight = $e.height();
$ghost.remove(); // cleanup
// console.log('Using a ghost got ',newHeight > originalHeight,' originalHeight=' + originalHeight + ' newHeight=' + newHeight)
return newHeight > originalHeight;
} //is_height_auto()
** Ghost element method explained (Previous answer):**
Greg Pettit had a pretty good answer in his blog, here is the main idea:
What’s unique about having auto height? Well, the fact that it allows height to change dynamically, of course!
Clone the element
Put it in visibility:hidden and position:absolute
Remove it's content
See if height changed (it should be around 0
now).
Cleanup
var isAutoHeight = function(element) {
// make a staging area for all our work.
$('body').append('');
// assume false by default
var autoHeight = false;
// clone the div and move it; get its height
var clone = element.clone();
clone.appendTo('#stage');
var initialHeight = clone.height();
// destroy all the content and compare height
clone.html('');
var currentHeight = clone.height();
if (currentHeight < initialHeight) {
autoHeight = true;
}
// get that clone and its smelly duplicate ID out of the DOM!
clone.remove();
// do the same for the stage
$('#stage').remove();
return autoHeight;
};

Ran into a bug using the method of clone->heightCheck->remove innerHTML->heightCompare. Where it does not register a change in height, even if the element has 100%/auto height.
Instead, this method appears to work:
let autoHeight = false;
// Set up stage area with 100% height/width
const stage = document.createElement('div');
stage.setAttribute('style', "position: relative; height: 100%; width: 100%;");
// Add stage to body
document.body.appendChild(stage);
// Clone the element and append to stage
const clone = element.cloneNode(false);
stage.appendChild(clone);
// Get Initial Height
const initialHeight = clone.offsetHeight;
// Squish content
stage.setAttribute('style', "position: relative; height: 1px; width: 1px;");
// Get new height
const currentHeight = clone.offsetHeight;
// Get max height (if it exists)
const hasMaxHeight = getComputedStyle(clone)["maxHeight"];
// Compare
if (currentHeight < initialHeight && hasMaxHeight == 'none') {
// Has 100% or auto height, and no maxHeight
} else if (hasMaxHeight !== 'none') {
// Flexible, but has a maxHeight
} else {
// Constrained by height size
}
// Remove elements
stage.remove();

Starting by Oriadam answer I created following jQuery function:
/**
* Checks if the element has explicitly set height by CSS styles.
* E.g.:
*
* var $myElement = jQuery('.my-element');
* if ($myElement.hasExplicitHeight()) {
* //...
* }
*
* This function is needed as .height() or .css('height') return a value, even
* if no height property was explicitly set using CSS.
*
* #returns {Boolean}
*/
jQuery.fn.hasExplicitHeight = function() {
var $element = jQuery(this);
var $clone = $element.clone();
$clone.html('');
$clone.css('visibility', 'hidden');
$clone.css('position', 'absolute');
$clone.insertAfter($element);
var hasExplicitHeight = $element.css('height') === $clone.css('height');
$clone.remove();
return hasExplicitHeight;
};
It works fine under condition that it is called only after the document is ready:
jQuery(function() {
// this code is launched only after the document is ready
jQuery('.my-element').hasExplicitHeight();
});

Related

Use matchMedia to enable/disable script like a media query on load/viewport resize

I only want to run a script when the viewport width is greater than a set value. I would also like this to check as the browser resizes and disable/enable as required. I've tried to achieve this using matchMedia and rather than checking every pixel, the script only triggers when the viewport is less/greater than a set value.
If I load a narrow viewport (less than 1080px) the JS doesn't trigger - perfect! Enlarging the viewport to have a width greater than 1080px then runs the script - also perfect!
The problem I have is when I scale down from a larger viewport (greater than 1080px) to narrow/small. The script still functions until I refresh the page - I'm hoping someone can help me with that.
As an aside, is it possible to change const mediaQuery = window.matchMedia('(min-width: 1080px)') to include a min-height or a more complex media query if required (not essential for this).
My script:
const mediaQuery = window.matchMedia('(min-width: 1080px)')
function viewportChange(e) {
// Check if the media query is true
if (e.matches) {
$(document).ready(function() {
var num_children = $('.split-loop__left').children().length;
var child_height = $('.split-loop__right').height() / num_children;
var half_way = num_children * child_height / 2;
$(window).scrollTop(half_way);
function crisscross() {
var parent = $(".split-loop");//.first();
var clone = $(parent).clone();
var leftSide = $(clone).find('.split-loop__left');
var rightSide = $(clone).find('.split-loop__right');
if (window.scrollY > half_way ) {
//We are scrolling up
$(window).scrollTop(half_way - child_height);
var firstLeft = $(leftSide).children().first();
var lastRight = $(rightSide).children().last();
lastRight.appendTo(leftSide);
firstLeft.prependTo(rightSide);
} else if (window.scrollY < half_way - child_height) {
var lastLeft = $(leftSide).children().last();
var firstRight = $(rightSide).children().first();
$(window).scrollTop(half_way);
lastLeft.appendTo(rightSide);
firstRight.prependTo(leftSide);
}
$(leftSide).css('bottom', '-' + window.scrollY + 'px');
$(rightSide).css('bottom', '-' + window.scrollY + 'px');
$(parent).replaceWith(clone);
}
$(window).scroll(crisscross);
});
}
}
// Register event listener
mediaQuery.addListener(viewportChange)
// Initial check
viewportChange(mediaQuery)

Javascript function run self on resize

I have a function, that I run on trigger through an element like $('.element').tileHeightInit();.
The function needs to run on window.load and window.resize. I guess I could define another function inside and run it there, but I figured there was a way to use this to run itself or something.
My function:
// master function for resizing tile elements
$.fn.tileHeightInit = function (
args = {
'breakPoint' : breakPoint,
'container' : container,
'squared' : squared,
'blockMargin' : blockMargin,
})
{
var breakPoint = args['breakPoint'],
container = args['container'],
squared = args['squared'],
blockMargin = args['blockMargin'];
var gridElements = $(this);
if (typeof(breakPoint)==='undefined') breakPoint = 767;
if (typeof(container)==='undefined') container = 'body';
if (typeof(squared)==='undefined') squared = true;
if (typeof(blockMargin)==='undefined') blockMargin = 30;
// return false if no elements found;
if($(container).find(gridElements).length < 1){
return false;
}
// run if window is larger than breakpoint
if($(window).width() >= breakPoint){
$(container).each(function(){
// height reset. Allows tiles to become smaller on risizing.
$(this).find(gridElements).each(function(){
$(this).height('auto');
})
// Get an array of all element heights
var elementHeights = $(this).find(gridElements).map(function() {
// block height datafield
var dataHeight = $(this).data('block-height');
// block widht datafield
var dataWidth = $(this).data('block-width');
// if squared is enabled
if(squared === true){
// if block is wider than tall, return width as height parameter for quadratic tiles
if($(this).height() < $(this).outerWidth() && dataWidth == 1){
// return height divided by data-height
return $(this).outerWidth();
} else{
// return width, since tile is wider than it's tall
return $(this).height() / dataHeight;
}
} else{
// return base height divided by block size;
return $(this).height() / dataHeight;
}
}).get();
// Math.max takes a variable number of arguments
// `apply` is equivalent to passing each height as an argument
var maxHeight = Math.max.apply(null, elementHeights);
// Set each height to the max height
$(this).find(gridElements).each(function(){
// variable for data-height attribute
var dataHeight = $(this).data('block-height');
// the missing margin the block would have had if split into more
var marginDiff = (dataHeight - 1) * blockMargin;
// set height to highest tile * data-height attribute + the margin difference between current block and surrounding blocks
$(this).height((maxHeight * dataHeight) + marginDiff);
});
})
} else{
// if smaller than breakpoint (colons collapsed)
$(container).find(gridElements).each(function(){
// reset height
$(this).height('auto');
})
}
} // END tileHeightInit();
$(window).resize(function(){
$('li').tileHeightInit({
'breakPoint': 500,
});
})
Basically I will be running this function on multiple elements, making writing them all into resize, load and possible more events seem inefficient.

How would you use anchor points in a div when using the flexcroll plugin?

I want to have a custom scrollbar on my main div which has buttons to go to certain parts of the div, however anchor points don't seem to work when using the flexcroll plugin (I know i'm doing anchor points correctly because when I disable flexcroll on that div they work fine)
Is their any method I could use to set up the anchor points?
EDIT FOUND SOLUTION: On the buttons I want to click to go to the specific place in the document I can put onclick="Wrapper.fleXcroll.setScrollPos(false,0);"
I've used this function in the past. FYI, I don't know anything about flexcroll, so this is not tested with that:
var isInt = function(val) {
return (parseInt(val, 10) == val);
};
var scrollTo = function(node) {
var pNode = node.parentNode;
var offset = node.offsetTop - pNode.offsetTop;
var pHeight = pNode.clientHeight;
var height = node.clientHeight;
var scrollOffset = pNode.scrollTop;
var buffer = 10;
var scroll = null;
if (scrollOffset > offset) {
scroll = offset - buffer;
} else if (pHeight + scrollOffset < offset + height + buffer) {
scroll = offset + height + buffer - pHeight;
}
if (isInt(scroll)) {
pNode.scrollTop = scroll;
}
};
This is the pure JS version. (example)
Here is an example of a jQuery version, which animates the scroll event: jQuery version

How to calculate an element's height considering collapsed margins

I want to calculate the total "height" of a div element considering the effect of collapsed margins because of child elements, that is, the total space that div element occupies in the document. I'm having a hard time thinking of the best algorithm / approach to do this.
Consider, for example, a div element with a margin-top of 10px and a height of 50px. This div element has a child <h2> element that has a margin-top of 20px. The div's margin will then collapse and the actual "height" of that div will be 70px. However, using jQuery methods, we are only able to get the height of the div without considering it's margins, or considering it's 10 pixel margin which would give us the wrong value:
$(elem).outerHeight() // 50
$(elem).outerHeight(true) // 60
To help illustrate my point, here is a jsfiddle I created with two examples.
My best guess at the moment is we have to iterate over all children of the div in some way and calculate the highest top and bottom margin.
According to what I understand from the W3C specification, we can skip this iteration for the top margin if the target div has a top-border-width or a top-padding. Ditto for the bottom margin.
Any help would be greatly appreciated. Thanks!
EDIT:
One (ugly) solution I thought about was wrapping the target div element in another div.
Then, we quickly add and remove a transparent borderTop and borderBottom to the wrapping div, measuring it's height in between. The borders will force the wrapping div's margin not to collapse with its children's margins. Something like this:
var collapsedHeight = function( target ) {
var $wrapper = $('<div />'),
$target = $(target);
$wrapper.insertAfter($target);
$target.appendTo($wrapper);
$wrapper.css({
borderTop: '1px solid transparent',
borderBottom: '1px solid transparent'
});
var result = $wrapper.outerHeight() - 2;
$target.insertAfter($wrapper);
$wrapper.remove();
return result;
};
I made a jsFiddle for it here.
Consider this hack:
$( elem ).wrap( '<div style="border:1px solid transparent;"></div>' ).parent().height()
The above expression returns 70 which is what you want, right?
So, the idea is to wrap your element in a DIV that has a transparent border set. This border will prevent the margins of your element to interfere with the margins of its previous and next sibling.
Once you get the height value, you can unwrap your element...
For a solution that doesn't involve DOM manipulation, you can achieve the same effect by adding padding to the element being measured and then removing it afterwards.
function getRealHeight(elementP) {
var
element = (elementP instanceof jQuery)? elementP : $(element),
padTop = parseInt(element.css('paddingTop')),
padBottom = parseInt(element.css('paddingBottom')),
offset,
height;
if (padTop == 0 || padBottom == 0) {
offset = 0;
if (padTop == 0) {
element.css('paddingTop', 1);
offset += 1;
}
if (padBottom == 0) {
element.css('paddingBottom', 1);
offset += 1;
}
height = (element.outerHeight(true) - offset);
if (padTop == 0) {
element.css('paddingTop', '');
}
if (padBottom == 0) {
element.css('paddingBottom', '');
}
} else {
height = element.outerHeight(true);
}
return height;
}
The bonus of this solution; you can sidestep the overhead of wrap/unwrap.
You can make it a jQuery plugin:
(function ($) {
$.fn.extend({
//plugin name - realHeight
realHeight: function (options) {
//Settings list and the default values
var defaults = {};
var options = $.extend(defaults, options);
function getRealHeight(elementP) {
var
element = (elementP instanceof jQuery) ? elementP : $(element),
padTop = parseInt(element.css('paddingTop')),
padBottom = parseInt(element.css('paddingBottom')),
offset,
height;
if (padTop == 0 || padBottom == 0) {
offset = 0;
if (padTop == 0) {
element.css('paddingTop', 1);
offset += 1;
}
if (padBottom == 0) {
element.css('paddingBottom', 1);
offset += 1;
}
height = (element.outerHeight(true) - offset);
if (padTop == 0) {
element.css('paddingTop', '');
}
if (padBottom == 0) {
element.css('paddingBottom', '');
}
} else {
height = element.outerHeight(true);
}
return height;
}
return getRealHeight($(this));
}
});
})(jQuery);

Need to calculate offsetRight in javascript

I need to calculate the offsetRight of a DOM object. I already have some rather simple code for getting the offsetLeft, but there is no javascript offsetRight property. If I add the offsetLeft and offsetWidth, will that work? Or is there a better way?
function getOffsetLeft(obj)
{
if(obj == null)
return 0;
var offsetLeft = 0;
var tmp = obj;
while(tmp != null)
{
offsetLeft += tmp.offsetLeft;
tmp = tmp.offsetParent;
}
return offsetLeft;
}
function getOffsetRight(obj)
{
if (obj == null)
return 0;
var offsetRight = 0;
var tmp = obj;
while (tmp != null)
{
offsetRight += tmp.offsetLeft + tmp.offsetWidth;
tmp = tmp.offsetParent;
}
return offsetRight;
}
Cannot be more simpler than this:
let offsetright = window.innerWidth - obj.offsetLeft - obj.offsetWidth
UPDATED POST TO CLARIFY SOME GOTCHAS:
// Assuming these variables:
const elem = document.querySelector('div'),
body = document.body,
html = document.documentElement;
Here are several approaches:
/* Leveraging the viewport AND accounting for possible overflow to the right */
const offsetRight = body.clientWidth - elem.getBoundingClientRect().right
// OR
const offsetRight = body.scrollWidth - elem.getBoundingClientRect().right
// OR
const offsetRight = html.scrollWidth - elem.getBoundingClientRect().right
OR
/*
* Likely the safest option:
* Doesn't depend on the viewport
* Accounts for overflow to the right
* Works even if the user is scrolled to the right some
* NOTE: This ends at the <html> element,
* but you may want to modify the code to end at the <body>
*/
const getOffsetRight = e => {
let left = e.offsetWidth + e.offsetLeft;
const traverse = eRef => {
eRef = eRef.offsetParent; // `.offsetParent` is faster than `.parentElement`
if (eRef) {
left += eRef.offsetLeft;
traverse(eRef);
}
};
traverse(e);
return html.scrollWidth - left;
};
const offsetRight = getOffsetRight(elem);
Import considerations:
Are you using box-sizing: border-box; for all your elements?
Is there margin-left set on the <body> or <html> elements you need to account for?
Does the <body> have a fixed width but centered such as with margin: 0 auto;
Those things will help determine which method to use, and if you want to modify the CSS and/or the JavaScript to account for those use cases.
ORIGINAL POST:
A few choices:
If you want "offsetRight" relative to the viewport, use element.getBoundingClientRect().right;
Your example is good simply subracting the parent width from the element's width + offsetLeft.
Lastly, to be relative to the document, and to speed up traversing (offsetParent):
In this example, I'm positioning a pseudo dropdown element below the
referenced element, but because I'm avoiding some tricky z-index
issues and want to have the element be referenced from the right and
expand out left, I had to append it to the body element, and the get
the "offsetRight" from the original parent.
...
// Set helper defaults
dropdownElem.style.left = 'auto';
dropdownElem.style.zIndex = '10';
// Get the elem and its offsetParent
let elem = dropdownElemContainer;
let elemOffsetParent = elem.offsetParent;
// Cache widths
let elemWidth = elem.offsetWidth;
let elemOffsetParentWidth = 0;
// Set the initial offsets
let top = elem.offsetHeight; // Because I want to visually append the elem at the bottom of the referenced bottom
let right = 0;
// Loop up the DOM getting the offsetParent elements so you don't have to traverse the entire ancestor tree
while (elemOffsetParent) {
top += elem.offsetTop;
elemOffsetParentWidth = elemOffsetParent.offsetWidth;
right += elemOffsetParentWidth - (elem.offsetLeft + elemWidth); // Most important line like your own example
// Move up the DOM
elem = elemOffsetParent;
elemOffsetParent = elemOffsetParent.offsetParent;
elemWidth = elemOffsetParentWidth;
}
// Set the position and show the elem
dropdownElem.style.top = top + 'px';
dropdownElem.style.right = right + 'px';
dropdownElem.style.display = 'block';
//Object references
function getObject(id) {
var object = null;
if (document.layers) {
object = document.layers[id];
} else if (document.all) {
object = document.all[id];
} else if (document.getElementById) {
object = document.getElementById(id);
}
return object;
}
//Get pixel dimensions of screen
function getDimensions(){
var winW = 630, winH = 460;
if (document.body && document.body.offsetWidth) {
winW = document.body.offsetWidth;
winH = document.body.offsetHeight;
}
if (document.compatMode=='CSS1Compat' && document.documentElement && document.documentElement.offsetWidth ) {
winW = document.documentElement.offsetWidth;
winH = document.documentElement.offsetHeight;
}
if (window.innerWidth && window.innerHeight) {
winW = window.innerWidth;
winH = window.innerHeight;
}
return{"width":winW, "height":winH}
}
//Get the location of element
function getOffsetRight(elem){
element=getObject(elem)
var width = element.offsetWidth
var right = 0;
while (element.offsetParent) {
right += element.offsetLeft;
element = element.offsetParent;
}
right += element.offsetLeft;
right = getDimensions()["width"]-right
right -= width
return right
}
This is not bullet-proof but you can usually get the "offsetRight" by calling: getOffsetRight("[object.id]")
If you are interested in using some Js library then try the following functionality of prototype js
http://api.prototypejs.org/dom/element/offset/

Categories