Search code examples
javascriptdomdrop-down-menunavigationappendchild

Dynamic horizontal nav bar where nav items "overflow" into a "more" dropdown when those nav items would normally wrap


I'm trying to modify this fiddle https://jsfiddle.net/xobea9cm/ as it pretty much has the exact function that I'm after, except the semantics of the html are not great.

In a nutshell, I'm after a horizontal navigation like so

|header and hero                                 |
|nav item 1   nav item 2    nav item 3   MORE v  |
|body copy                                       |

Within "more" there would be "nav item 4" and "nav item 5". It would change with screen size. As you reduce the browser width, "nav item 3" would be moved into "more", as you increase the browser width, the nav items within "more" would flow back out when horizontal space permits.

The original version html is "divitus" and looks like:

<nav class="nav">
    <div id="menu" class="menu">
        <div class="menuitem">One</div>
        <div class="menuitem">Two</div>
        <div class="menuitem">Three</div>
        <div class="menuitem">Four</div>
        <div class="menuitem">Five</div>
        <div class="menuitem">Six</div>
        
        <div class="more-items">
            <div id="more" class="more-item menuitem">More</div>
            <div class="menuitem">Seven</div>
            <div class="menuitem">Eight</div>
            <div class="menuitem">Nine</div>
        </div>
    </div>
</nav>

My version is here: https://jsfiddle.net/oywkdr6e/ where my html currently looks like:

<nav class="nav-more">
    <ul class="nav-more-menu">
        <li class="nav-more-menu-item">nav item one</li>
        <li class="nav-more-menu-item"><a href="#two">nav item two</a></li>
        <li class="nav-more-menu-item"><a href="#three">nav item three</a></li>
        <li class="nav-more-menu-item"><a href="#four">nav item four</a></li>
        <li class="nav-more-menu-item"><a href="#five">nav item five</a></li>
        <li class="nav-more-menu-item"><a href="#six">nav item six</a></li>
        
        <li class="more">
            <!--THIS IS WHAT I'M TRYING TO CHANGE-->
            <li class="more-trigger nav-more-menu-item">More</li>
            <li class="nav-more-menu-item"><a href="#seven">nav item seven</a></li>
            <li class="nav-more-menu-item"><a href="#eight">nav item eight</a></li>
        </li>
    </ul>
</nav>

What I want is this:

<nav class="nav-more">
    <ul class="nav-more-menu">
        <li class="nav-more-menu-item">nav item one</li>
        <li class="nav-more-menu-item"><a href="#two">nav item two</a></li>
        <li class="nav-more-menu-item"><a href="#three">nav item three</a></li>
        <li class="nav-more-menu-item"><a href="#four">nav item four</a></li>
        <li class="nav-more-menu-item"><a href="#five">nav item five</a></li>
        <li class="nav-more-menu-item"><a href="#six">nav item six</a></li>
        
        <li class="more">
            <!--THIS IS WHAT I WANT-->
            <button class="more-trigger">More</button>
            <ul class="more-items">
                <li class="nav-more-menu-item"><a href="#seven">nav item seven</a></li>
                <li class="nav-more-menu-item"><a href="#eight">nav item eight</a></li>
            </ul>
        </li>
    </ul>
</nav>

The current functionality has the "more" menu displaying on a hover of <li class="more">, but I eventually want that available on a button trigger for accessibility, so I'll just show/hide that button's sibling <ul class="more-items"> and position it like a dropdown menu.

I've tried several times to update the JS for the new markup but I seem to always end up at the same place. On page load, it looks good, but when I resize the browser width, my DOM gets appended with <ul class="more-items"> on every resize event.

How would you fix this to output the desired markup? There's something I'm just not getting with updating the DOM. Here is the JS:

const parser = new DOMParser();

function dealWithMenu(navMoreMenu) {
  //console.log('dealWithMenu()');
  let navLIs = navMoreMenu.querySelectorAll('li');
  let moreLI = parser.parseFromString('<li class="more"></li>', 'text/html').body.firstChild;
  //console.log('navLIs:', navLIs);
  //console.log('moreLI:', moreLI);
  //console.log('navMoreMenu:', navMoreMenu);

  let count = 0;

  for (var i = navLIs.length; i--;) {
    let $this = navLIs[i];

    //show at least two nav items
    if (count >= navLIs.length - 2) {
      continue;
    }

    //if trailing nav item offsetTop value is different than first nav item, trailing nav item has wrapped to a new line, so move into "more" dropdown
    if ($this.offsetTop > navLIs[0].offsetTop || moreLI.offsetTop > navLIs[0].offsetTop) {
      navMoreMenu.appendChild(moreLI);
      moreLI.insertBefore($this, moreLI.firstChild);
      count++;
    } else {
      i = 0;
    }
  }

  //we have overflow nav items in "more" dropdown
  if (moreLI.children.length) {
    console.log('moreLI.children.length', moreLI.children.length);
    moreLI.insertBefore(
      parser.parseFromString('<li class="more-trigger nav-more-menu-item">More</li>', 'text/html').body.firstChild,
      moreLI.firstChild
    );
  }

  moreLI.addEventListener('click', (e) => {
    console.log('more click:', e.target);
  });
}

function shorterMenu() {
  //console.log('shorterMenu()');
  const navMoreMenu = document.querySelector('.nav-more-menu');
  const moreLI = navMoreMenu.querySelector('.more');
  const moreTrigger = navMoreMenu.querySelector('.more-trigger');
  //console.log('moreLI:', moreLI);
  //console.log('moreTrigger:', moreTrigger);

  moreTrigger?.remove();    

  if (moreLI != undefined && moreLI.children?.length > 0) {
    Array.from(moreLI.children).forEach(child => moreLI.parentElement.appendChild(child));
    moreLI.remove();
  }

  dealWithMenu(navMoreMenu);
}

shorterMenu();
window.addEventListener('resize', () => shorterMenu());

Solution

  • In dealWithMenu():

    • Add the More <ul> and get a reference to it:
      let moreLI = parser.parseFromString('<li class="more"><ul class="more-items"></ul></li>', 'text/html').body.firstChild;
      const [moreList] = moreLI.children;
      
    • In the for loop of navLIs, append overflowing elements into the moreList:
      moreList.appendChild($this);
      
    • Have the logic of modifying the DOM be based on the moreList:
      if (moreList.children.length) {
        console.log('moreLI.children.length', moreList.children.length);
      
    • Change the trigger element to the desired <button> element:
      moreLI.insertBefore(
        parser.parseFromString('<button class="more-trigger">More</button>', 'text/html').body.firstChild,
        moreLI.firstChild
      );
      

    In shorterMenu():

    • Get a list of the <li> elements inside the more menu:
      const moreLI = navMoreMenu.querySelector('.more');
      const moreElements = moreLI?.querySelectorAll('li');
      
    • Conditionally move the <li> elements back and remove the moreLI depending on the moreElements variable:
      if (moreLI != undefined && moreElements?.length > 0) {
        moreElements.forEach(child => navMoreMenu.appendChild(child));
        moreLI.remove();
      }
      

    const parser = new DOMParser();
    
    function dealWithMenu(navMoreMenu) {
      //console.log('dealWithMenu()');
      let navLIs = navMoreMenu.querySelectorAll('li');
      let moreLI = parser.parseFromString('<li class="more"><ul class="more-items"></ul></li>', 'text/html').body.firstChild;
      const [moreList] = moreLI.children;
      //console.log('navLIs:', navLIs);
      //console.log('moreLI:', moreLI);
      //console.log('navMoreMenu:', navMoreMenu);
    
      let count = 0;
    
      for (var i = navLIs.length; i--;) {
        let $this = navLIs[i];
    
        //show at least two nav items
        if (count >= navLIs.length - 2) {
          continue;
        }
    
        //if trailing nav item offsetTop value is different than first nav item, trailing nav item has wrapped to a new line, so move into "more" dropdown
        if ($this.offsetTop > navLIs[0].offsetTop || moreLI.offsetTop > navLIs[0].offsetTop) {
          navMoreMenu.appendChild(moreLI);
          moreList.appendChild($this);
          count++;
        } else {
          i = 0;
        }
      }
    
      //we have overflow nav items in "more" dropdown
      if (moreList.children.length) {
        console.log('moreLI.children.length', moreList.children.length);
        moreLI.insertBefore(
          parser.parseFromString('<button class="more-trigger">More</button>', 'text/html').body.firstChild,
          moreLI.firstChild
        );
      }
    
      moreLI.addEventListener('click', (e) => {
        console.log('more click:', e.target);
      });
    }
    
    function shorterMenu() {
      //console.log('shorterMenu()');
      const navMoreMenu = document.querySelector('.nav-more-menu');
      const moreLI = navMoreMenu.querySelector('.more');
      const moreElements = moreLI?.querySelectorAll('li');
      const moreTrigger = navMoreMenu.querySelector('.more-trigger');
      //console.log('moreLI:', moreLI);
      //console.log('moreTrigger:', moreTrigger);
    
      moreTrigger?.remove();    
    
      if (moreLI != undefined && moreElements?.length > 0) {
        moreElements.forEach(child => navMoreMenu.appendChild(child));
        moreLI.remove();
      }
    
      dealWithMenu(navMoreMenu);
    }
    
    shorterMenu();
    window.addEventListener('resize', () => shorterMenu());
    html,
    body {
      margin: 0;
      padding: 0;
    }
    
    body {
      background: #ccc;
    }
    
    header {
      background: #fff;
      height: 100px;
      width: 100%;
      position: fixed;
      top: 0;
      border: 2px solid red;
      z-index: 10;
    }
    
    header,
    .hero {
      display: flex;
    }
    
    header p,
    .hero p {
      margin: auto;
    }
    
    .hero {
      background: #999;
      width: 100%;
      height: 300px;
    }
    
    main {
      border: 2px solid blue;
      background: #fff;
      height: 2000px;
      max-width: 1200px;
      margin: 100px auto 2rem auto;
    }
    
    
    .nav-more {
      border: 1px solid red;
      background: #efefef;
      padding: 1.5rem 6rem;
      position: sticky;
      top: 100px;
      display: flex;
    }
    
    .nav-more-menu {
      border: 1px solid blue;
      display: inline-flex;
      flex-wrap: wrap;
      list-style: none;
      margin: 0;
      padding: 0;
    }
    
    .nav-more-menu-item {
      /*border: 1px solid white;*/
      /*background: black;*/
      /*color: white;*/
      padding: 5px 10px;
    }
    
    .nav-more-menu-item a {
      /*color: white;*/
    }
    
    
    .more {
      background: dodgerblue;
      color: white;
      max-width: 200px;
    }
    
    .more:hover {
      background: deepskyblue;
    }
    
    .more li {
      display: none;
    }
    
    .more li:first-of-type {
      display: block;
      cursor: pointer;
    }
    
    .more:hover>li {
      display: block;
    }
    <header><p>fixed header</p></header>
            
    <main>
      <div class="hero"><p>hero</p></div>
    
      <nav class="nav-more">
        <ul class="nav-more-menu">
          <li class="nav-more-menu-item">nav item one</li>
          <li class="nav-more-menu-item"><a href="#two">nav item two</a></li>
          <li class="nav-more-menu-item"><a href="#three">nav item three</a></li>
          <li class="nav-more-menu-item"><a href="#four">nav item four</a></li>
          <li class="nav-more-menu-item"><a href="#five">nav item five</a></li>
          <li class="nav-more-menu-item"><a href="#six">nav item six</a></li>
          <li class="nav-more-menu-item"><a href="#seven">nav item seven</a></li>
          <li class="nav-more-menu-item"><a href="#eight">nav item eight</a></li>
        </ul>
      </nav>
    
      <div style="padding:0 6rem;">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    
        <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.</p>
    
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    
        <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.</p>
      </div>
    </main>