Reputation: 113
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.net/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 complex solutions.
Upvotes: 9
Views: 5184
Reputation: 60875
This is what I'm thinking:
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 computedStyle = getComputedStyle(domNode)
var verticalBorderWidth = parseInt(computedStyle[outerBorderWidth]) + parseInt(computedStyle[innerBorderWidth]) +
parseInt(computedStyle[outerPaddingWidth]) + parseInt(computedStyle[innerPaddingWidth]) +
parseInt(computedStyle[outerMarginWidth]) + parseInt(computedStyle[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.org/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.org/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* compute 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 computedStyles = getComputedStyle(domNode)
var result = {}
properties.forEach(function(prop) {
result[prop] = computedStyles[prop]
})
if(parent) {
parent.style.display = originalDisplay
}
return result
}
// from lostsource http://stackoverflow.com/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'
// computedStyles - (Optional) Pass in the domNodes computed styles if you already have it (since I hear its somewhat expensive)
function hasScrollBars(domNode, dimension, computedStyles) {
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 = computedStyles || 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.
Upvotes: 1
Reputation: 113
Here is the best solution I could come 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.net/p8wfejhr/
Upvotes: 0
Reputation: 1673
If 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.
Upvotes: 1