admin管理员组

文章数量:1122847

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="/[email protected]/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="/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

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/[email protected]/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/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>

What am I missing?

Share Improve this question edited Dec 11, 2024 at 11:39 Mark Rotteveel 109k225 gold badges155 silver badges218 bronze badges asked Nov 30, 2024 at 14:34 MeekMeek 3,3549 gold badges42 silver badges71 bronze badges 2
  • Check the image in the top of the post - the order of the items is not correct. I marked the wrong item with a red box. You might have to resize your window. – Meek Commented Nov 30, 2024 at 15:23
  • 1 you could have simply written that your elements are out of order, and used clearer labels in your LIs – Mister Jojo Commented Nov 30, 2024 at 15:54
Add a comment  | 

1 Answer 1

Reset to default 2

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/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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>

本文标签: javascriptDetecting overflowing menu items doesn39t always calculate correctlyStack Overflow