javascriptcsstwitter-bootstrap

Detecting overflowing menu items doesn't always calculate correctly


I'm trying to calculate if a horizontal menu has space for all menu items, and if not, wrap overflowing items inside a dropdown.

Here is what I have. It works - almost - because sometimes is doesn't calculate correctly. Try resizing the window, you'll see something like this: (on this figure the elements doesn't stay in there original order)

const nav = document.querySelector(".navbar");
const contentBar = nav.querySelector(".navbar-nav");
const navItems = document.querySelectorAll(".navbar-nav > li:not(.grouped)");
const allItems = contentBar.querySelectorAll('li');
const dropdown = nav.querySelector(".grouped-content");
const more = nav.querySelector(".grouped");

const update = () => {
    // Show all items for exact calculation
    allItems.forEach((item) => {
        item.classList.remove('d-none');
    });

    let avaliableWidth = nav.offsetWidth;
    let stopWidth = more.offsetWidth;
    let subitems = [];
    dropdown.innerHTML = "";

    navItems.forEach((item, i) => {
        // Calculate if menu items are wider than navbar
        if (avaliableWidth > stopWidth + item.offsetWidth) {
            stopWidth += item.offsetWidth;
        } else {
            // Not enough space
            let li = document.createElement("li");
            li.innerHTML = item.innerHTML;
            // Append overflowing menu items to dropdown
            dropdown.appendChild(li);
            // Hide items that won't fit in menu
            item.classList.add('d-none');
            // Count items in dropdown
            subitems.push(i);
        }
    })

    // Hide "More" item if it has no children
    if (!subitems.length) {
        more.classList.add('d-none');
    } 
}

update();
window.addEventListener("resize", update);
.nav-link {
    white-space: nowrap;
    max-width: 100px;
    text-overflow: ellipsis;
    overflow: hidden;
}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
 <div class="container">
     <nav class="navbar navbar-expand-sm">
         <ul class="navbar-nav">
             <li class="nav-item"><a href="#" class="nav-link">Item 1</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 22</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 333</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 4444</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 5555555</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 66</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 7777777777777</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 8</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 999999</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 10101010</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 1111</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 12121212</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 131313131313</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 1414</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 15</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 16161616</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 1717</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 18181818181818</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 191919</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 20</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 212121</a></li>
                <li class="nav-item"><a href="#" class="nav-link">Item 2222</a></li>
                <li class="nav-item dropdown grouped">
                    <a href="#" class="nav-link dropdown-toggle" aria-haspopup="true" aria-expanded="false" role="button" data-bs-toggle="dropdown" data-bs-display="static" data-bs-auto-close="outside">More</a>
                    <ul class="dropdown-menu grouped-content"></ul>
                </li>
            </ul>
        </nav>
    </div>
 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

What am I missing?


Solution

  • your error :

    if (avaliableWidth > stopWidth + item.offsetWidth) {
    

    change to (look at parenthesis)

    if (avaliableWidth > (stopWidth + item.offsetWidth)) {
    

    This all about Operator precedence


    A cleanest way to do this...

    (_this will also work with bootstrap media queries_)
    First JS Code version:
    const 
      nav        = document.querySelector('.navbar')  //  nav
    , contentBar = nav.querySelector('.navbar-nav')   //  nav > ul
    , moreGroup  = nav.querySelector('.navbar-nav > li.grouped')
    , navItems   = [...nav.querySelectorAll('.navbar-nav > li:not(.grouped)')]
    , moreList   = moreGroup.querySelector('ul.grouped-content')
      ;
    sizingNav();
    window.addEventListener('resize', sizingNav);
    
    function sizingNav()
      {
      let avaliable_width = nav.offsetWidth;
    
      while (moreList.lastElementChild)  // clear the "More" List
        moreList.removeChild(moreList.lastElementChild);
    
      moreGroup.classList.add('d-none');
      navItems.forEach( item => item.classList.remove('d-none') );
    
      for (let i = navItems.length; i-- > 0;)    // right to left movin to set
        {                                       // elements into the 'More' list
        if ( contentBar.offsetWidth > avaliable_width )
          {
          let li           = document.createElement('li');
              li.innerHTML = navItems[i].innerHTML;
    
          moreList.prepend(li);
          navItems[i].classList.add('d-none');
          moreGroup.classList.remove('d-none');
          }
        else break;   // no needs to go further...
        }
      }
    

    const 
      nav        = document.querySelector('.navbar')  //  nav
    , contentBar = nav.querySelector('.navbar-nav')   //  nav > ul
    , moreGroup  = nav.querySelector('.navbar-nav > li.grouped')
    , navItems   = [...nav.querySelectorAll('.navbar-nav > li:not(.grouped)')]
    , moreList   = moreGroup.querySelector('ul.grouped-content')
      ;
    sizingNav();
    window.addEventListener('resize', sizingNav);
    
    function sizingNav()
      {
      let avaliable_width = nav.offsetWidth;
    
      while (moreList.lastElementChild)  // clear the "More" List
        moreList.removeChild(moreList.lastElementChild);
    
      moreGroup.classList.add('d-none');
      navItems.forEach( item => item.classList.remove('d-none') );
    
      for (let i = navItems.length; i-- > 0;)    // right to left movin to set
        {                                       // elements into the 'More' list
        if ( contentBar.offsetWidth > avaliable_width )
          {
          let li           = document.createElement('li');
              li.innerHTML = navItems[i].innerHTML;
    
          moreList.prepend(li);
          navItems[i].classList.add('d-none');
          moreGroup.classList.remove('d-none');
          }
        else break;   // no needs to go further...
        }
      }
    .nav-link {
      white-space   : nowrap;
      max-width     : 100px;
      text-overflow : ellipsis;
      overflow      : hidden;  
      }
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
      
    <div class="container">
      <nav class="navbar navbar-expand-sm">
        <ul class="navbar-nav">
          <li class="nav-item"><a href="#" class="nav-link">Item 1</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 2-2</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 3-33</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 4-444</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 5-555555</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 6-6</a></li>  
          <li class="nav-item"><a href="#" class="nav-link">Item 7-777777777777</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 8</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 9-99999</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 10+101010</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 11+11</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 12+121212</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 13+1313131313</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 14+14</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 15+</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 16+161616</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 17+17</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 18+181818181818</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 19+1919</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 20+</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 21+2121</a></li> 
          <li class="nav-item"><a href="#" class="nav-link">Item 22+22</a></li>
          <li class="nav-item dropdown grouped">
            <a href="#" class="nav-link dropdown-toggle" aria-haspopup="true" aria-expanded="false" role="button" data-bs-toggle="dropdown" data-bs-display="static" data-bs-auto-close="outside">More</a>
            <ul class="grouped-content"></ul>
          </li>
        </ul>
      </nav>
    </div>


    [EDIT]
    I'm a little surprised that this post can please, while this question is down-voted.

    My first answer here was just made to show the OP a better approach than his calculation by successive additions.

    But as my answer visibly arouses interest, I prefer to leave "to prosperity" this new answer, which avoids create/delete LI+insertHTML, when it is enough just to readdress them directly in the right place.

    Please, give a positive score to the OP. This question was really badly asked at the beginning, but it has been corrected; and this one may have interest on SO.


    PS: As for the use of the d-none class, it is completely useless since the arrival of the 2 CSS pseudo-classes: :has and :empty...

    see css addition

    LI.grouped:has( UL.grouped-content:empty ) {
      display : none;
      }
    

    New version:

    const 
      nav        = document.querySelector('.navbar')  //  nav
    , contentBar = nav.querySelector('.navbar-nav')   //  nav > ul
    , moreGroup  = nav.querySelector('.navbar-nav > li.grouped')
    , navItems   = [...nav.querySelectorAll('.navbar-nav > li:not(.grouped)')]
    , moreList   = moreGroup.querySelector('ul.grouped-content')
      ;
    sizingNav();
    window.addEventListener('resize', sizingNav);
    
    function sizingNav()
      {
      let avaliable_width = nav.offsetWidth
        ;
      while (moreList.firstElementChild)  // replace all `LI.nav-item`
        {                                // back from moreList to navbar-nav
        moreList.firstElementChild.classList.add('nav-item');
        moreGroup.before(moreList.firstElementChild);  // so, it is a move just before
        }
      for (let i = navItems.length; i-- > 0;)    // right to left movin to set
        {                                       // elements into the 'More' list
        if ( contentBar.offsetWidth > avaliable_width )
          {
          navItems[i].classList.remove('nav-item');
          moreList.prepend(navItems[i]);
          }
        else break;   // no needs to go further...
        }
      }
    

    const 
      nav        = document.querySelector('.navbar')  //  nav
    , contentBar = nav.querySelector('.navbar-nav')   //  nav > ul
    , moreGroup  = nav.querySelector('.navbar-nav > li.grouped')
    , navItems   = [...nav.querySelectorAll('.navbar-nav > li:not(.grouped)')]
    , moreList   = moreGroup.querySelector('ul.grouped-content')
      ;
    sizingNav();
    window.addEventListener('resize', sizingNav);
    
    function sizingNav()
      {
      let avaliable_width = nav.offsetWidth
        ;
      while (moreList.firstElementChild)  // replace all `LI.nav-item`
        {                                // back from moreList to navbar-nav
        moreList.firstElementChild.classList.add('nav-item');
        moreGroup.before(moreList.firstElementChild);  // so, it is a move just before
        }
      for (let i = navItems.length; i-- > 0;)    // right to left movin to set
        {                                       // elements into the 'More' list
        if ( contentBar.offsetWidth > avaliable_width )
          {
          navItems[i].classList.remove('nav-item');
          moreList.prepend(navItems[i]);
          }
        else break;   // no needs to go further...
        }
      }
    .nav-link {
      white-space   : nowrap;
      max-width     : 100px;
      text-overflow : ellipsis;
      overflow      : hidden;  
      }
    LI.grouped:has( UL.grouped-content:empty ) {
      display : none;
      }
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
    
    <div class="container">
      <nav class="navbar navbar-expand-sm">
        <ul class="navbar-nav">
          <li class="nav-item"><a href="#" class="nav-link">Item 1</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 2-2</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 3-33</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 4-444</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 5-555555</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 6-6</a></li>  
          <li class="nav-item"><a href="#" class="nav-link">Item 7-777777777777</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 8</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 9-99999</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 10+101010</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 11+11</a></li>
          <li class="nav-item"><a href="#" class="nav-link">Item 12+121212</a></li>
          <li class="nav-item dropdown grouped">
            <a href="#" class="nav-link dropdown-toggle" aria-haspopup="true" aria-expanded="false" role="button" data-bs-toggle="dropdown" data-bs-display="static" data-bs-auto-close="outside">More</a>
            <ul class="grouped-content"></ul>
          </li>
        </ul>
      </nav>
    </div>