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?
your error :
if (avaliableWidth > stopWidth + item.offsetWidth) {
change to (look at parenthesis)
if (avaliableWidth > (stopWidth + item.offsetWidth)) {
This all about Operator precedence
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>
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;
}
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>