That's what I think:
- Find the closest ancestor with apparent height
- Find all the ancestors with a percentage height and calculate the height of the nearest of these ancestors to find the available height. Lets name this ancestor NAR and NARH height.
- Find the distance your element is from the top of its parent (using getBoundingClientRect). Call him DT
- Subtract the upper bound of the NAR from the DT. Call it A.
- Maximum height must be NARH-A
Something similar could be done for a minimum.
UPDATE: Ohhh, I implemented this idea and it works! It has a lot of crap that it takes into account, including margins, borders, indents, scrollbars (even with custom widths), percentage widths, maximum height / width, and sister nodes. Check 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 probably put this in the npm / github module, because it seems like it should be accessible naively, but it is not, and it takes a lot of effort to work properly.