1

I'm new to coding, I am have a made a decent looking website (https://garibpathshala.in/) with a toggle nav menu for mobiles.

is there any way so that if we click outside the menu it'll close the menu.

Please have a look at my code and help me :)

HTML

      <ul class="header-nav-links">
    <li class="active"><a href="https://garibpathshala.in">HOME</a></li>
    <li><a href="#projects_section">PROJECTS</a></li>
    <li><a href="#meet_the_team_section">TEAM</a></li>
    <li><a href="#about_us_section">ABOUT</a></li>
    <li><a href="https://gallery.garibpathshala.in">GALLERY</a></li>
    <li><a href="https://contact.garibpathshala.in">CONTACT</a></li>
    <li><a href="https://donate.garibpathshala.in">DONATE</a></li>
    <li><a href="https://join.garibpathshala.in">JOIN US</a></li>
   </ul>
  
   <div class="burger">
      <div line1></div>
      <div line2></div>
      <div line3></div>
    </div>

JS

const burger = document.querySelector(".burger");
const navLinks = document.querySelector(".header-nav-links");
const links = document.querySelectorAll(".header-nav-links li");

//Toggle Nav
burger.addEventListener("click", () => {
    navLinks.classList.toggle("open");

//Animate Links
links.forEach((link, index) => {
    if (link.style.animation) {
        link.style.animation = ""
    }else{
        link.style.animation = `navLinkFade 0.5s ease forwards ${index / 7+0.2}s`;
    }
});
});

Here is a screenshot of the nav menu

Subham Manna
  • 103
  • 1
  • 12
  • Please do some research on your own. This is a community to help each other. Asking a very personalized question for a personalized answer is not encouraged here. [Here](https://jsfiddle.net/jedimasterbruce/tfyhw96a/8/) is one pretty straight forward example Here is another one [Here](https://stackoverflow.com/questions/57259093/how-do-i-outside-click-to-close-this-custom-offcanvas-nav-from-bootstraps-docs) –  Aug 09 '20 at 06:21

3 Answers3

0

You could add a click listener on body or document and rely on event delegation to take the appropriate action, as in the sample code below.

(See the in-code comments for further clarification.)

// Selects some DOM elements and converts the collection to an array
const listItems = Array.from(document.querySelectorAll(".header-nav-links > li"));

// Calls `handleMenuDisplay` when anything is clicked
document.addEventListener("click", handleMenuDisplay);

// Defines `handleMenuDisplay`
function handleMenuDisplay(event){ // Listeners can access their triggering events
  const clickedThing = event.target; // The event's `target` property is useful

  // Depending on what was clicked, takes an appropriate action
  if(listItems.includes(clickedThing)){ // Arrays have an `includes` method
    openMenu(clickedThing);
  }
  else{
    closeMenu();
  }
}

function openMenu(clickedLi){
  demo.textContent = clickedLi.dataset.demoText;
}

function closeMenu(){
  demo.textContent = "Menu is closed";
}
li{ margin: 7px; padding: 3px; border: 1px solid grey; }
#demo{ margin-left: 2ch; font-size: 1.5em; font-weight: bold; }
<ul class="header-nav-links">
  <li data-demo-text="Home menu is open">HOME</li>
  <li data-demo-text="Projects menu is open">PROJECTS</li>
  <li data-demo-text="Team menu is open">TEAM</li>
  <li data-demo-text="About menu is open">ABOUT</li>
</ul>

<p id="demo">Menu is closed</p>

Note: My use of custom data-attributes was just to make the sample code a bit cleaner -- it's not part of event delegation, and the display text for each li could just have easily been written out manually in the script.

Cat
  • 4,141
  • 2
  • 10
  • 18
  • This solves the problem for multiple child-elements in known/identified containers, but in my opinion is hard to read and also extend (It would also require you to do this for every single place you have a dropdown menu, this works for section specific things). Another thing is your use of `function` to declare methods **AFTER** they're called, this is a nasty practice and makes for nightmare spaghetti JS code, order of precedence is important for context! We're not writing C here, no `extern` or foward declaration needed. – SeedyROM Aug 09 '20 at 10:41
  • Also relying on a click to the body element could be caught be other `event.stopPropagation`s / `event.preventDefault`s, this might work in edge cases but will bite you when you need to make this portable. – SeedyROM Aug 09 '20 at 10:47
0

Based on the other solutions:

An even simpler way is to use the focus and blur states of DOM elements to handle the state for your menu.

document.querySelectorAll('.menu').forEach((menu) => {
    const items = menu.querySelector('.menu-items');

    menu.addEventListener('click', (e) => {
        items.classList.remove("hide");
        menu.focus(); // Probably redundant but just in case!
    });

    menu.addEventListener('blur', () => {
        items.classList.add("hide");
    });
});
.menu {
    cursor: pointer;
    display: inline-block;
}

.menu > span {
    user-select: none;
}

.menu:focus {
    outline: none;
    border: none;
}

.hide {
    display: none;
}

.menu-items > * {
    user-select: none;
}
<div class="menu" tabindex="0">
    <span>Menu +</span>
    <div class="menu-items hide">
        <div>Item 0</div>
        <div>Item 1</div>
        <div>Item 2</div>
    </div>
</div>

The secret here is to give the .menu div a tabindex so that it's focusable by the browser.

First things first for the JS:

  • Search the page for any instance of the menu class document.querySelectorAll
  • Get the items element that's currently hidden by .hide
  • When somebody clicks on the menu remove the .hide class from the items
    • This should focus the div, but just in case menu.focus is called!
  • Then whenever somebody loses focus of the div, AKA clicks away etc. Add the .hide class back.

This could be expanded to change the text of the button, do all sorts of other things. It also keeps your code super clean because you're relying on the browsers own internal state management, so you don't have to do any checks.

Handle a second click close

Right so it works great, but in order to make it function as most people expect UI we need to close it when the menu div is clicked again too (replace span with any class you need):

...
    menu.querySelector('span').addEventListener('click', (e) => {
        e.stopPropagation();
        menu.blur();
    });

    menu.addEventListener('click', (e) => {
        items.classList.remove("hide");
        menu.focus(); // Probably redundant but just in case!
    });

    menu.addEventListener('blur', () => {
        items.classList.add("hide");
    });
...
SeedyROM
  • 2,203
  • 1
  • 18
  • 22
  • Also this is maybe kind of a meta (hypermeta) programming thing but declaring all of our DOM instances in a `forEach` makes it explicitly clear what's going on. No smoke, no mirrors right? We're defining a simple pattern that can be implemented in other components. As long as our class names and hierarchies are setup as excepted we can reuse this for any component regardless of style. – SeedyROM Aug 09 '20 at 11:18
0

You could remove "open" class from the menu if the event.CurrentTarget is not the hamburger menu and anything else in the document (html or body) is clicked.

You would also need to stopImmediatePropagation on the .hamburger and navLinks itself to stop those from being registered as clicks to the body, since they are children of the body and the event would otherwise bubble up to the body. MDN reference: https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles

const burger = document.querySelector(".burger");
const navLinks = document.querySelector(".header-nav-links");
const links = document.querySelectorAll(".header-nav-links li");
const body = document.querySelector('html');

//Toggle Nav
burger.addEventListener("click", (e) => {
    navLinks.classList.toggle("open");
    e.stopImmediatePropagation();

    //Animate Links
    links.forEach((link, index) => {
        if (link.style.animation) {
            link.style.animation = "";
        }else{
            link.style.animation = `navLinkFade 0.5s ease forwards ${index / 7+0.2}s`;
        }
    });
});

navLinks.addEventListener("click", (eve) => {
     eve.stopImmediatePropagation();
});

body.addEventListener("click", (ev) => {

      if (ev.currentTarget != burger) {
          navLinks.classList.remove("open");
      }
});
.burger {
    display: block;
    cursor:pointer;
}

.header-nav-links {
    display: block;
}

.header-nav-links.open {
    transform: translateX(0%);
}

 
.header-nav-links {
    right: 0;
    position: fixed;
    height: 92vh;
    top: 16vh;
    background-color: rgba(0, 0, 0, 0.7);
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 50%;
    transform: translateX(100%);
    transition: transform 0.5s ease-in;
}

.header-nav-links li {
    list-style-type: none;
}

.header-nav-links li:hover {
    border: 1px solid #fff;
    border-radius: 6pc;
    background-color: #007bff;
}

.header-nav-links a {
    color: whitesmoke;
    text-decoration: none;
    font-family: Arial, sans-serif;
    font-weight: normal;
    font-size: 16px;
    border: 0px solid white;
    transition: 400ms;
    padding: 5px 15px;
    border-radius: 19px; 
}
<ul class="header-nav-links">
    <li class="active"><a href="https://garibpathshala.in">HOME</a></li>
    <li><a href="#projects_section">PROJECTS</a></li>
    <li><a href="#meet_the_team_section">TEAM</a></li>
    <li><a href="#about_us_section">ABOUT</a></li>
    <li><a href="https://gallery.garibpathshala.in">GALLERY</a></li>
    <li><a href="https://contact.garibpathshala.in">CONTACT</a></li>
    <li><a href="https://donate.garibpathshala.in">DONATE</a></li>
    <li><a href="https://join.garibpathshala.in">JOIN US</a></li>
</ul>
  
<div class="burger">
  BURGER
  <div line1></div>
  <div line2></div>
  <div line3></div>
</div>
Cat
  • 4,141
  • 2
  • 10
  • 18
JohnnyP
  • 426
  • 1
  • 7
  • 12
  • This does work but is honestly kind of a hack in my opinion, to each their own though! This is probably harder to scale as well. – SeedyROM Aug 09 '20 at 10:29
  • 1
    But very straightforward for the OP (a professed beginner) to implement while also being exposed to basic concepts related to event target and bubbling, An elaboration on why it is kind of a hack might also be productive to the OP’s (and my) edification — the ultimate goal. – JohnnyP Aug 09 '20 at 16:21
  • Copy and paste is not the ultimate goal for a beginner, and your solution is not only technically sketchy but also unintuitive/missing edge cases. You also describe 0% of why this works and has some hard edge cases. If you can read my other comment, relying on a body "click" event can be a bad idea since there could be other event `stopPropagation`s called. – SeedyROM Aug 09 '20 at 16:34
  • Thank you for the clarifying your opinion. That’s helpful, as mentioned, edification is the ultimate goal. – JohnnyP Aug 09 '20 at 18:58