1

I have a Bootstrap navbar. Inspecting with Chrome DevTools I noticed that this (and other browsers, I suppose) returns decimal box widths instead of integers (i.e. 83.06 instead of 83).

decimal width

For a pixel-perfect alignment with some other elements I need to 'convert to integer' this number, and to avoid button collapse in 2+ words items I need to 'round up' it: in short, 83.06 should become 84 (and not 83).

The only way I'm aware of is with JavaScript, so I did this script:

$(window).on('resize', function(){ // recalculate when resizing window
  if (window.innerWidth > 768) { // only in 'desktop' version(s), smaller devices don't need recalculation
    $('.navbar-nav').find('> li:not(.hidden-sm)').each(function(){
      var width = $(this).children('a').outerWidth(); // for each valid 'li', take its 'a' descendant width
      console.log(width); // watch what happens
      $(this).width(Math.ceil(width)); // change item width with a 'rounded up' value
    })
  }
}).resize();

Here is the code snippet (jsFiddle):

$(window).on('resize', function(){ // recalculate when resizing window
  if (window.innerWidth > 768) { // only in 'desktop' version(s)
    $('.navbar-nav').find('> li:not(.hidden-sm)').each(function(){
      var width = $(this).children('a').outerWidth(); // for each valid 'li', take its 'a' descendant width
      console.log(width); // watch what happens
      $(this).width(Math.ceil(width)); // change item width with a 'rounded up' value
    })
  }
}).resize();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">Link One<span class="sr-only">(current)</span></a></li>
        <li><a href="#">Link Two, Long</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
          </ul>
        </li>
        <li class="hidden-sm"><a href="#">Link Three: too long to fit, hide on small devices</a></li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

Pretty straight, right? But it doesn't work as expected: items' original widths (86.05, 124.17 and 107.7 according to Chrome DevTools) are not always rounded up despite the Math.ceil() function and I end up with 86, 124 and 108 (only the latter is right; the first two items 'collapse' because there is not enough space). Shouldn't Math.ceil() always round up? Very weird...

"Ok", I thought, "let's find a workaround": so I swapped Math.ceil() with parseInt() and add at least +1 pixel, like this other snippet (jsFiddle):

$(window).on('resize', function(){ // recalculate when resizing window
  if (window.innerWidth > 768) { // only in 'desktop' version(s)
    $('.navbar-nav').find('> li:not(.hidden-sm)').each(function(){
      var width = $(this).children('a').outerWidth(); // for each valid 'li', take its 'a' descendant width
      console.log(width); // watch what happens
      $(this).width(parseInt(width)+1); // change item width with a 'rounded up' value
    })
  }
}).resize();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">Link One<span class="sr-only">(current)</span></a></li>
        <li><a href="#">Link Two, Long</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
          </ul>
        </li>
        <li class="hidden-sm"><a href="#">Link Three: too long to fit, hide on small devices</a></li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>

Good, buttons don't collapse but... try to resize the window: buttons grow forever, maybe because I'm adding 1 pixel to each iteration: this is worse than before...

Now I am stuck and can't imagine other ways to go: any solution to get integers rounded up without ending with all these crazy results, please? Thank you

myfunkyside
  • 3,890
  • 1
  • 17
  • 32
Ivan
  • 2,463
  • 6
  • 39
  • 51

1 Answers1

1

Your problem is that jQuery's .width() already internally rounds the width values to the closest integer (so: 86.05 => 86 , 124.17 => 124 , 107.7 => 108).

So the values you retrieve using .width() and perform your rounding calculations on, are already integers, and therefor they have no effect whatsoever:)

This can be resolved by using JavaScript's native .getBoundingClientRect():
This will return the actual floating-point width (got it from this answer).

var width = $(this).children('a')[0].getBoundingClientRect().width;

You could leave it at that, but now the widths of all those lis are always reset, whether they already have been or not.
To prevent that, I used an if-clause to check if the width is not already an integer, and only then gets the element actually resized.

This is all you need for that (which I got from this answer):

if (width%1 !== 0) {$(this).width(Math.ceil(width));}

But to be safe I would also remove .children('a'), and measure the lis themselves:

var width = $(this)[0].getBoundingClientRect().width;

Apparently the a automatically fills the entire li, but the li also auto-scales to the a, and since you're performing the .width() operation on the li, it seems safer and more logical to also check the width of that same element.
Alternatively, you could set the width() of the a instead of the li.


See code snippet for a demo  (put snippet in "Full page" to test):

$(window).on('resize', function(){ // recalculate when resizing window
  if (window.innerWidth > 768) { // only in 'desktop' version(s)
    $('.navbar-nav').find('> li:not(.hidden-sm)').each(function(){
      var width = $(this)[0].getBoundingClientRect().width; // for each valid 'li', take its 'a' descendant width
      if (width%1 !== 0) {$(this).width(Math.ceil(width));} // if width isn't already an integer, round up
      console.log(width, 'resize: '+(width%1 !== 0));
    });
  }
}).resize();
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li class="active"><a href="#">Link One<span class="sr-only">(current)</span></a></li>
        <li><a href="#">Link Two, Long</a></li>
        <li class="dropdown">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
          <ul class="dropdown-menu">
            <li><a href="#">Action</a></li>
            <li><a href="#">Another action</a></li>
            <li><a href="#">Something else here</a></li>
          </ul>
        </li>
        <li class="hidden-sm"><a href="#">Link Three: too long to fit, hide on small devices</a></li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-fluid -->
</nav>
jsfiddle: https://jsfiddle.net/c7927ud1/6/
myfunkyside
  • 3,890
  • 1
  • 17
  • 32