4

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.

vitorbal
  • 2,871
  • 24
  • 24
  • 1
    Have you considered avoiding the scenarios where the margin of an ancestor overrides the margin of the element in question? It seems like bad design to me, I wouldn't want that to occur on my pages... – Šime Vidas Oct 27 '11 at 14:05
  • Hi Šime Vidas, I completely agree, However I guess my question was more when considering we have no control over the page design. When this code would be in a bookmarklet, for example. – vitorbal Oct 27 '11 at 15:07

3 Answers3

5

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...

Šime Vidas
  • 182,163
  • 62
  • 281
  • 385
  • Hey Šime Vidas, thanks a lot for the answer. As you can see, I edited my original post with the exact same idea just one minute after you :) I supposed it's an ugly hack but the only way to do it that i can think of.. – vitorbal Oct 27 '11 at 15:28
  • @vitorbal What a funny coincidence `:)` – Šime Vidas Oct 27 '11 at 15:29
  • @ŠimeVidas - the link to the "live demo" is not so live anymore :( – vsync Oct 12 '14 at 17:15
0

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.

Robert Steward
  • 153
  • 1
  • 10
0

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);
KurtJ
  • 19
  • 2