The Problem: Given a DIV element with a fixed height, which contains an unknown number of child elements that are sized relative to its height, calculate the maximum/minimum height that the DIV could resize to, without violating any of the maximum/minimum values of its child elements.
Example Find the maximum/minimum height of DIV A
Answer
Minimum: 150px
Maximum: 275px
* {
box-sizing: border-box;
}
.border {
border-style: solid;
border-width: 1px 1px 1px 1px;
}
.A {
height: 200px;
width: 200px;
}
.B {
float: left;
width: 50%;
height: 75%;
min-height: 125px;
max-height: 225px;
background: yellow;
}
.C {
float: left;
width: 50%;
height: 75%;
min-height: 100px;
max-height: 250px;
background: green;
}
.D{
float: left;
width: 100%;
height: 25%;
min-height: 25px;
max-height: 50px;
background: blue;
}
<div class="A border">
<div class="B border">
B
</div>
<div class="C border">
C
</div>
<div class="D border">
D
</div>
</div>
The Problem: Given a DIV element with a fixed height, which contains an unknown number of child elements that are sized relative to its height, calculate the maximum/minimum height that the DIV could resize to, without violating any of the maximum/minimum values of its child elements.
Example Find the maximum/minimum height of DIV A
Answer
Minimum: 150px
Maximum: 275px
* {
box-sizing: border-box;
}
.border {
border-style: solid;
border-width: 1px 1px 1px 1px;
}
.A {
height: 200px;
width: 200px;
}
.B {
float: left;
width: 50%;
height: 75%;
min-height: 125px;
max-height: 225px;
background: yellow;
}
.C {
float: left;
width: 50%;
height: 75%;
min-height: 100px;
max-height: 250px;
background: green;
}
.D{
float: left;
width: 100%;
height: 25%;
min-height: 25px;
max-height: 50px;
background: blue;
}
<div class="A border">
<div class="B border">
B
</div>
<div class="C border">
C
</div>
<div class="D border">
D
</div>
</div>
Additional Information: I currently have tried using an algorithm that traverses the DIV's DOM tree and creates an object graph representing the spacial positioning of the elements, using the elements offset. Below is a rudimentary algorithm that examines the spacial relationship of the elements, allowing for a 10px spread between edges to be considered 'touching'.
jQuery and other libraries are allowed as long as they are open source.
var _isContentRoot = function(a,b){
var aRect = a.innerRect;
var bRect = b.outerRect;
//Check if child element is a root node
return Math.abs(aRect.top - bRect.top) <= 10;
}
var _isLayoutSibling = function(a,b){
var aRect = a.outerRect;
var bRect = b.outerRect;
// If element X has a boundary that intersects element Y, and
// element X is located above element Y, element Y is a child node of
// element X
if(Math.abs(aRect.bottom - bRect.top) <= 10) {
if (aRect.left <= bRect.left && aRect.right >= bRect.left ||
aRect.left <= bRect.right && aRect.right >= bRect.right ||
aRect.left >= bRect.left && aRect.right <= bRect.right ||
aRect.left <= bRect.left && aRect.right >= bRect.right) {
return true;
}
}
return false;
}
Edit: Fixed CSS error. Here is an updated Fiddle http://jsfiddle/zqnscmo2/
Edit 2: Try to think of this more of a graph problem in the problem space of CSS/HTML. Imagine the CSS and HTML are used to describe a graph where each DIV is a vertex. There exists an edge between the two vertices
1.) if the HTML element's bounding rectA.top ≈ rectB.top OR 2.) there exists an edge if the bounding rectA.bottom ≈ rectB.top
Each vertex has two exclusive sets of edges, set A contains all edges that meet criterion 1. Set B contains all edges that meet criterion 2. Therefor you can traverse the graph and find the minimal and maximal path and that should be the PARENT DIV's max/min height.
This is my proposed algorithm for determining the max/min height of the inner contents. I'm very much open to less plex solutions.
Share Improve this question edited Dec 17, 2014 at 16:23 lndigo asked Dec 16, 2014 at 22:02 lndigolndigo 1131 silver badge6 bronze badges 7-
Interesting question. Note that there's a syntax error in your
.C
style:float: left: width: 50%;
. When that's corrected,.C
will actually drop below.B
. Since it's floated, it won't contribute to the height of.A
. You can see this if you add a colored border to.A
. – Rick Hitchcock Commented Dec 16, 2014 at 22:17 - I have created a fiddle containing your basic setup (without css errors) and put together a small function as a starting point: jsfiddle/a5njkr00/1 - I think you should give getBoundingClientRect a chance. – axel.michel Commented Dec 16, 2014 at 22:53
- @axel.michel, I appreciate your input. Keep in mind that DIV A could be containing another DIV that does not have a min-height or max-height declared, but must have its max/min values derived from its contents, therefor any solution is going to need to be recursive in nature. Here is a reply fiddle jsfiddle/z8u0saqc – lndigo Commented Dec 17, 2014 at 15:11
- so if I understand you correctly, you want to find the min & max height of children divs so that you can set the parent divs min & max height? – Adjit Commented Dec 17, 2014 at 15:26
- @Adjit, that is correct. The min and max height of the parent DIV cannot be known at design time since its inner content its generated at runtime, however, the javascript needs to be able to determine the maximum and minimum values the DIV can resize to based on the inner content, which can be arbitrarily plex. – lndigo Commented Dec 17, 2014 at 15:48
3 Answers
Reset to default 1If I understood your question correctly, would this work?
// - I use two support functions that can probably be found in other JSes frameworks, and they're down below.
function calculateMySizes(someElement) {
var childDiv = findChild(someElement, "DIV");
var totalWidth = 0;
var totalHeight = 0;
var maxWidth = 0;
var maxHeight = 0;
do
{
if(childDiv.offsetLeft > maxWidth) {
maxWidth = childDiv.offsetLeft;
totalWidth += childDiv.offsetLeft;
}
if(childDiv.offsetTop > maxHeight) {
maxHeight = childDiv.offsetTop;
totalHeight += childDiv.offsetTop;
}
}
while (childDiv = nextElement(childDiv));
alert("object's current width is: " + totalWidth + " and it's child's largest width is: " + maxWidth);
alert("object's current height is: " + totalHeight + " and it's child's largest height is: " + maxHeight);
}
// - Returns the next Element of object
function nextElement(object) {
var nextObject = object;
while (nextObject = nextObject.nextSibling) {
if (nextObject.nodeType == 1) {
return nextObject;
}
}
return nextObject;
}
// - Returns the first child of elementName found
function findChild(object, elementName) {
for (var i = 0; i < object.childNodes.length; i++) {
if (object.childNodes[i].nodeType == 1) {
if (object.childNodes[i].nodeName.toUpperCase() == childName) {
return object;
}
if (object.childNodes[i].hasChildNodes()) {
var child = findChild(object.childNodes[i], childName, countMatch);
if (child) {
return child;
}
}
}
}
}
I can think of a scenario where the child object's bounding box is deceptively smaller than it's own children, in the case of a float or position:absolute element, and to fix that a recursive call for all the children would be required, but other than this scenario, this should give you the minimum width/height of any element according to their children's sizes.
This is what I'm thinking:
- Find the nearest ancestor with an explicit height
- Find all the ancestors with percentage heights and calculate the height of the nearest one of those ancestors to find the available height. Lets call that ancestor NAR and the height NARH.
- Find the distance your element is from the top of its parent (with getBoundingClientRect). Call it DT
- Subtract the top boundary of NAR from DT. Call this A.
- Your maximum height should be NARH-A
Something similar could be done for the minimum.
UPDATE: Ohhhh kay, I implemented this idea and it works! There's a lot of crap it takes into account including margins, borders, padding, scroll bars (even with custom widths), percentage widths, max-height/width, and sibling nodes. Check out this code:
exports.findMaxHeight = function(domNode) {
return findMaxDimension(domNode,'height')
}
exports.findMaxWidth = function(domNode) {
return findMaxDimension(domNode,'width')
}
// finds the maximum height/width (in px) that the passed domNode can take without going outside the boundaries of its parent
// dimension - either 'height' or 'width'
function findMaxDimension(domNode, dimension) {
if(dimension === 'height') {
var inner = 'Top'
var outer = 'Bottom'
var axis = 'Y'
var otherAxis = 'X'
var otherDimension = 'width'
} else {
var inner = 'Left'
var outer = 'Right'
var axis = 'X'
var otherAxis = 'Y'
var otherDimension = 'height'
}
var maxDimension = 'max'+dimension[0].toUpperCase()+dimension.slice(1)
var innerBorderWidth = 'border'+inner+'Width'
var outerBorderWidth = 'border'+outer+'Width'
var innerPaddingWidth = 'padding'+inner
var outerPaddingWidth = 'padding'+outer
var innerMarginWidth = 'margin'+inner
var outerMarginWidth = 'margin'+outer
var overflowDimension = 'overflow'+axis
var propertiesToFetch = [
dimension,maxDimension, overflowDimension,
innerBorderWidth,outerBorderWidth,
innerPaddingWidth,outerPaddingWidth,
innerMarginWidth, outerMarginWidth
]
// find nearest ancestor with an explicit height/width and capture all the ancestors in between
// find the ancestors with heights/widths relative to that one
var ancestry = [], ancestorBottomBorder=0
for(var x=domNode.parentNode; x!=null && x!==document.body.parentNode; x=x.parentNode) {
var styles = getFinalStyle(x,propertiesToFetch)
var h = styles[dimension]
if(h.indexOf('%') === -1 && h.match(new RegExp('\\d')) !== null) { // not a percentage and some kind of length
var nearestAncestorWithExplicitDimension = x
var explicitLength = h
ancestorBottomBorder = parseInt(styles[outerBorderWidth]) + parseInt(styles[outerPaddingWidth])
if(hasScrollBars(x, axis, styles))
ancestorBottomBorder+= getScrollbarLength(x,dimension)
break;
} else {
ancestry.push({node:x, styles:styles})
}
}
if(!nearestAncestorWithExplicitDimension)
return undefined // no maximum
ancestry.reverse()
var maxAvailableDimension = lengthToPixels(explicitLength)
var nodeToFindDistanceFrom = nearestAncestorWithExplicitDimension
ancestry.forEach(function(ancestorInfo) {
var styles = ancestorInfo.styles
var newDimension = lengthToPixels(styles[dimension],maxAvailableDimension)
var possibleNewDimension = lengthToPixels(styles[maxDimension], maxAvailableDimension)
var moreBottomBorder = parseInt(styles[outerBorderWidth]) + parseInt(styles[outerPaddingWidth]) + parseInt(styles[outerMarginWidth])
if(hasScrollBars(ancestorInfo.node, otherAxis, styles))
moreBottomBorder+= getScrollbarLength(ancestorInfo.node,otherDimension)
if(possibleNewDimension !== undefined && (
newDimension !== undefined && possibleNewDimension < newDimension ||
possibleNewDimension < maxAvailableDimension
)
) {
maxAvailableDimension = possibleNewDimension
nodeToFindDistanceFrom = ancestorInfo.node
// ancestorBottomBorder = moreBottomBorder
} else if(newDimension !== undefined) {
maxAvailableDimension = newDimension
nodeToFindDistanceFrom = ancestorInfo.node
// ancestorBottomBorder = moreBottomBorder
} else {
}
ancestorBottomBorder += moreBottomBorder
})
// find the distance from the top
var putedStyle = getComputedStyle(domNode)
var verticalBorderWidth = parseInt(putedStyle[outerBorderWidth]) + parseInt(putedStyle[innerBorderWidth]) +
parseInt(putedStyle[outerPaddingWidth]) + parseInt(putedStyle[innerPaddingWidth]) +
parseInt(putedStyle[outerMarginWidth]) + parseInt(putedStyle[innerMarginWidth])
var distanceFromSide = domNode.getBoundingClientRect()[inner.toLowerCase()] - nodeToFindDistanceFrom.getBoundingClientRect()[inner.toLowerCase()]
return maxAvailableDimension-ancestorBottomBorder-verticalBorderWidth-distanceFromSide
}
// gets the pixel length of a value defined in a real absolute or relative measurement (eg mm)
function lengthToPixels(length, parentLength) {
if(length.indexOf('calc') === 0) {
var innerds = length.slice('calc('.length, -1)
return caculateCalc(innerds, parentLength)
} else {
return basicLengthToPixels(length, parentLength)
}
}
// ignores the existences of 'calc'
function basicLengthToPixels(length, parentLength) {
var lengthParts = length.match(/(-?[0-9]+)(.*)/)
if(lengthParts != null) {
var number = parseInt(lengthParts[1])
var metric = lengthParts[2]
if(metric === '%') {
return parentLength*number/100
} else {
if(lengthToPixels.cache === undefined) lengthToPixels.cache = {}//{px:1}
var conversion = lengthToPixels.cache[metric]
if(conversion === undefined) {
var tester = document.createElement('div')
tester.style.width = 1+metric
tester.style.visibility = 'hidden'
tester.style.display = 'absolute'
document.body.appendChild(tester)
conversion = lengthToPixels.cache[metric] = tester.offsetWidth
document.body.removeChild(tester)
}
return conversion*number
}
}
}
// https://developer.mozilla/en-US/docs/Web/CSS/number
var number = '(?:\\+|-)?'+ // negative or positive operator
'\\d*'+ // integer part
'(?:\\.\\d*)?'+ // fraction part
'(?:e(?:\\+|-)?\\d*)?' // scientific notation
// https://developer.mozilla/en-US/docs/Web/CSS/calc
var calcValue = '(?:'+
'('+number+')'+ // length number
'([A-Za-z]+|%)?'+ // optional suffix (% or px/mm/etc)
'|'+
'(\\(.*\\))'+ // more stuff in parens
')'
var calcSequence = calcValue+
'((\\s*'+
'(\\*|/|\\+|-)'+
'\\s*'+calcValue+
')*)'
var calcSequenceItem = '\\s*'+
'(\\*|/|\\+|-)'+
'\\s*'+calcValue
var caculateCalc = function(calcExpression, parentLength) {
var info = calcExpression.match(new RegExp('^'+calcValue))
var number = info[1]
var suffix = info[2]
var calcVal = info[3]
var curSum = 0, curProduct = getCalcNumber(number, suffix, calcVal, parentLength), curSumOp = '+'
var curCalcExpression = calcExpression.slice(info[0].length)
while(curCalcExpression.length > 0) {
info = curCalcExpression.match(new RegExp(calcSequenceItem))
var op = info[1]
number = info[2]
suffix = info[3]
calcVal = info[4]
var length = getCalcNumber(number,suffix,calcVal, parentLength)
if(op in {'*':1,'/':1}) {
curProduct = calcSimpleExpr(curProduct,op,length)
} else if(op === '+' || op === '-') {
curSum = calcSimpleExpr(curSum,curSumOp,curProduct)
curSumOp = op
curProduct = length
}
curCalcExpression = curCalcExpression.slice(info[0].length)
}
curSum = calcSimpleExpr(curSum,curSumOp,curProduct)
return curSum
}
function calcSimpleExpr(operand1, op, operand2) {
if(op === '*') {
return operand1 * operand2
} else if(op === '/') {
return operand1 / operand2
} else if(op === '+') {
return operand1 + operand2
} else if(op === '-') {
return operand1 - operand2
} else {
throw new Error("bad")
}
}
function getCalcNumber(number, suffix, calcVal, parentLength) {
if(calcVal) {
return caculateCalc(calcVal, parentLength)
} else if(suffix) {
return basicLengthToPixels(number+suffix, parentLength)
} else {
return number
}
}
// gets the style property as rendered via any means (style sheets, inline, etc) but does *not* pute values
// domNode - the node to get properties for
// properties - Can be a single property to fetch or an array of properties to fetch
function getFinalStyle(domNode, properties) {
if(!(properties instanceof Array)) properties = [properties]
var parent = domNode.parentNode
if(parent) {
var originalDisplay = parent.style.display
parent.style.display = 'none'
}
var putedStyles = getComputedStyle(domNode)
var result = {}
properties.forEach(function(prop) {
result[prop] = putedStyles[prop]
})
if(parent) {
parent.style.display = originalDisplay
}
return result
}
// from lostsource http://stackoverflow./questions/13382516/getting-scroll-bar-width-using-javascript
// dimension - either 'width' or 'height'
function getScrollbarLength(domNode, dimension) {
if(dimension === 'width') {
var offsetDimension = 'offsetWidth'
} else {
var offsetDimension = 'offsetHeight'
}
var outer = document.createElement(domNode.nodeName)
outer.className = domNode.className
outer.style.cssText = domNode.style.cssText
outer.style.visibility = "hidden"
outer.style.width = "100px"
outer.style.height = "100px"
outer.style.top = "0"
outer.style.left = "0"
outer.style.msOverflowStyle = "scrollbar" // needed for WinJS apps
domNode.parentNode.appendChild(outer)
var lengthNoScroll = outer[offsetDimension]
// force scrollbars with both css and a wider inner div
var inner1 = document.createElement("div")
inner1.style[dimension] = "120%" // without this extra inner div, some browsers may decide not to add scoll bars
outer.appendChild(inner1)
outer.style.overflow = "scroll"
var inner2 = document.createElement("div")
inner2.style[dimension] = "100%"
outer.appendChild(inner2) // this must be added after scroll bars are added or browsers are stupid and don't properly resize the object (or maybe they do after a return to the scheduler?)
var lengthWithScroll = inner2[offsetDimension]
domNode.parentNode.removeChild(outer)
return lengthNoScroll - lengthWithScroll
}
// dimension - Either 'y' or 'x'
// putedStyles - (Optional) Pass in the domNodes puted styles if you already have it (since I hear its somewhat expensive)
function hasScrollBars(domNode, dimension, putedStyles) {
dimension = dimension.toUpperCase()
if(dimension === 'Y') {
var length = 'Height'
} else {
var length = 'Width'
}
var scrollLength = 'scroll'+length
var clientLength = 'client'+length
var overflowDimension = 'overflow'+dimension
var hasVScroll = domNode[scrollLength] > domNode[clientLength]
// Check the overflow and overflowY properties for "auto" and "visible" values
var cStyle = putedStyles || getComputedStyle(domNode)
return hasVScroll && (cStyle[overflowDimension] == "visible"
|| cStyle[overflowDimension] == "auto"
)
|| cStyle[overflowDimension] == "scroll"
}
I'll probably put this in an npm/github module cause it seems like something that should be available naively, but isn't and takes a shiteload of work to do right.
Here is the best solution I could e up with.
First, if a DIV depends on it's child's contents to determine it's size, I give it an the selector .childDependent and if the div can resize vertically, I give it the selector .canResize.
<div class="A border childDependent canResize">
<div class="B border canResize">
B
</div>
<div class="C border canResize">
C
</div>
<div class="E border canResize">
E
</div>
<div class="D border canResize">
D
</div>
</div>
Here is a fiddle to look at: http://jsfiddle/p8wfejhr/