How can we close a toggle menu with an outside click - javascript

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">HOME</li>
<li>PROJECTS</li>
<li>TEAM</li>
<li>ABOUT</li>
<li>GALLERY</li>
<li>CONTACT</li>
<li>DONATE</li>
<li>JOIN US</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

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">HOME</li>
<li>PROJECTS</li>
<li>TEAM</li>
<li>ABOUT</li>
<li>GALLERY</li>
<li>CONTACT</li>
<li>DONATE</li>
<li>JOIN US</li>
</ul>
<div class="burger">
BURGER
<div line1></div>
<div line2></div>
<div line3></div>
</div>

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.

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");
});
...

Related

Navigation animation using JS and SCSS, can you tell me where I'm going wrong?

I am trying to create a nav that contains 2 elements (left and right). I would like both elements to move in from their respective edges of the screen, meeting in the middle to create a full screen nav. Could anyone shed some light on where I'm going wrong?
So far I have managed to get each box to move individually but not both at the same time. I gave them both separate ID's within their nav class thinking this would allow me to manipulate their postioning in different ways when the function is called that toggles 'expanded' but again this hasnt worked.
This is my HTML Structure:
<div class="nav-toggle">
<div class="nav-toggle-bar"></div>
</div>
<nav id="left" class="nav">
</nav>
<nav id="right" class="nav">
<ul>
<li>Portfolio</li>
<li>Services</li>
<li>Contact</li>
</ul>
</nav>
Here is an part of my SCSS:
.nav {
background: $nav-background;
color: $nav-color;
cursor: pointer;
font-size: 2rem;
height: 100vh;
padding: 6rem 2rem 2rem 2rem;
position: fixed;
top: 0;
width: $nav-width;
z-index: $z-nav;
ul {
#include center-vertically;
list-style: none;
margin: 0;
padding: 0;
}
}
#right{
#include transition(right 0.5s ease);
right: -$nav-width;
&.expanded { right: 0; }
}
#left{
#include transition(left 0.5s ease);
left: -$nav-width;
&.expanded { left: 0; }
}
And this is my JS:
(function() {
var hamburger = {
navToggle: document.querySelector('.nav-toggle'),
nav: document.querySelector('nav'),
doToggle: function(e) {
e.preventDefault();
this.navToggle.classList.toggle('expanded');
this.nav.classList.toggle('expanded');
}
};
hamburger.navToggle.addEventListener('click', function(e) { hamburger.doToggle(e); });
}());
This is the codepen I have been working on: https://codepen.io/matthewoproctor/pen/YmxZPQ
Thanks in advance for any help.
Your approach with the two IDs and the shared classname is perfect, as is your SCSS. The problem here is actually pretty simple: hamburger.nav is using document.querySelector, which only selects the first element with the nav class. Instead, you should use document.querySelectorAll. Then you just have to tweak the doToggle function to loop through all elements with the nav class.
(function() {
var hamburger = {
navToggle: document.querySelector('.nav-toggle'),
nav: document.querySelectorAll('nav'),
doToggle: function(e) {
e.preventDefault();
this.navToggle.classList.toggle('expanded');
this.nav.forEach(n => n.classList.toggle('expanded'));
}
};
hamburger.navToggle.addEventListener('click', function(e) {
hamburger.doToggle(e);
});
}());

On button click display list items, dropdown list should not hide until button is clicked again

I have looked everywhere possible as I am trying to develop a drop down button, but instead of options then the buttons display unordered list items, but when a user click off the button the the button does not close, but in order to close the button, then the button needs to be click again.
Down below you will find the way the button is when not clicked and the way the button appears when it has been clicked.
If you also go to the following website you will see an example of the button in action by click "See our list of websites"
Button on a website for example
Any help would be greatly appreciated and thanks in advance.
Here you go, key functions I used;
.click() for the a tag or the link, the function inside the .click() will be called.
.slideToggle() the ul after the click, this would hide or show the target element depending on its state.
Then add positon:absolute to the ul so that it wouldn't affect inline elements.
$(document).ready(function() {
$(".toggle-button").click(function() {
$(this).parent().find("ul").slideToggle(function() {
// Animation complete.
});
});
})
.links-unordered {
display: inline-block;
position: relative;
}
.links-unordered {
margin-top: 20px;
min-height: 30px;
}
.links-unordered .toggle-button {
text-decoration: none;
padding: 12px 16px 12px 16px;
transition: 0.2s;
border: 1px solid black;
}
.links-unordered .toggle-button:hover,
.links-unordered .toggle-button:active,
.links-unordered .toggle-button:focus,
.links-unordered .toggle-button:visited {
text-decoration: none;
color: black;
}
.links-unordered .toggle-button:hover {
border-width: 2px;
}
.links-unordered ul {
position: absolute;
top: 10px;
margin-top: 25px;
padding-inline-start: 20px;
}
.links-unordered ul li {
line-height: 25px;
padding-left: 15px;
}
.links-unordered a {
text-decoration: none;
color: black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="links-unordered">
<a class="toggle-button" href="#">SEE OUR LIST OF WEBSITES</a>
<ul style="display:none;">
<li>cdn.sc.rockstargames.com</li>
<li>lifeinvader.com</li>
<li>rockstargames.com</li>
<li>socialclub.rockstargames.com</li>
</ul>
</div>
<div class="links-unordered">
<a class="toggle-button" href="#">SEE OUR LIST OF WEBSITES</a>
<ul style="display:none;">
<li>cdn.sc.rockstargames.com</li>
<li>lifeinvader.com</li>
<li>rockstargames.com</li>
<li>socialclub.rockstargames.com</li>
</ul>
</div>
I'm not sure if I understand what you want. But here's a sample of what you asked, a button that when you click, show a list. And when you click on an item, the list goes out and you have the item. Hope this helps you. It's a simple code, but if you have questions, go ahead and ask!!
function closeList(e) {
var site = e.target.innerText;
alert(site + ' clicked!!');
document.querySelector('#dvSites').style.display = 'none';
}
function showList() {
var dvSites = document.querySelector('#dvSites');
if (dvSites.style.display === '')
return; // already visible
dvSites.style.display = '';
}
// Add eventListener to close the div
var lis = document.querySelector('#dvSites').querySelectorAll('li');
for(var i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', closeList);
}
// Add eventListener to open the div
document.querySelector('#btnShow').addEventListener('click', showList);
<button id="btnShow">Show sites!!</button>
<div id="dvSites" style="display: none">
<ul>
<li>stackoverflow.com</li>
<li>www.google.com</li>
<li>www.sipmann.com</li> <!-- :) -->
</ul>
</div>

Use a Click Event to Add and Remove a Class to individual loop items

I want to add a CSS class to a nav menu item each time it's clicked (so i can then style that menu item so people know this is the page they are on). I know I need to loop through the .menu-item class and add / remove a new CSS class using a loop, but I can't seem to get it to play ball.
I'm guessing I need to make the .menu-item the 'this' object and use a boolean to add or remove the CSS class, dependent on whether the currentItem variable is set to true or false.
I can't seem to get this to play though and I'm not 100% sure I'm using the event listener in the correct way.
Any help would be awesome.
codepen: https://codepen.io/emilychews/pen/eeKPoo?editors=1010
JS
var navlinks = document.getElementsByClassName('menu-item')
var currentItem = false;
for (i = 0; i<navlinks.length; i+=1) {
function addCurrentItemToMenu() {
if (currentItem === false) {
navlinks = this
this.classList.add('current-item')
currentItem = true
} else {
this.classList.remove('current-item')
currentItem = false
}
}
}
navlinks.addEventListener("click", function(){
addCurrentItemToMenu()
}, false)
CSS
body, ul {padding: 0; margin: 0}
#main-header {width: 100%; height: 100px;}
#mobile-menu-button {display: none;}
#main-navigation {
position: relative;
width: 100%;
height: 100px;
background: red;
display: flex;
justify-content: space-between;
box-sizing: border-box;
padding: 10px 5% 10px 5%;
align-items: center;
}
ul#nav-menu-items {
display: flex;
margin-left: auto;
}
#main-navigation ul li {list-style-type: none;}
ul#nav-menu-items li a {
padding: 10px 15px;
margin: 0 5px;
box-sizing: border-box;
background: yellow;
text-decoration: none;
color:#000;
}
#main-navigation ul#nav-menu-items li a:hover {
color:blue;
transition: color .25s;
}
HTML
<header id="main-header">
<nav id="main-navigation">
<ul id="nav-menu-items">
<li class="menu-item">News</li>
<li class="menu-item">About</li>
<li class="menu-item">Contact</li>
</ul>
</nav>
</header>
Here is a modified codepen for your problem: https://codepen.io/anon/pen/RjJEWe?editors=1010
The code simply add current-item class to the clicked anchor, so you can style it however you want in css. You can also see event.preventDefault() in the code to prevent anchor from following your link, as it will reload the page and js won't do anything. It depends on the stack that you are using. If you have server backed website, the current link will be handled by the server and html returned will already have the class set appropriately, if you have frontend js framework (Angular, VueJS, ReactJS), you must handle it appropriately.
Just for your example you can see the code below:
var navlinks = document.querySelectorAll('li.menu-item > a');
// Loop through all the links and add event listener
navlinks.forEach(function(item) {
item.addEventListener('click', function(event) {
event.preventDefault();
// Remove the class from all elements
navlinks.forEach(function(item) {
item.classList.remove('current-item');
})
// add the class to the current one
this.classList.add('current-item');
});
});

Dropdown Menu using javascript, HTML and CSS

I can't seem to get this to work. I don't wanna put jQuery in yet. Just doing plain javascript. When I click on the image nothing happens. I need it to dropdown the navigation when I click the image. Edited my Javascript code. I added alert to show the current status of what class the toggle is using. But still I cant get to change it from header_navigation_mobile to header_navigation_mobile.is_open
This is my HTML CODE for the Clickable Image
<a href="#" onclick="toggleMenu()">
<img class="mobile_navigation_button" src="{{site.baseurl}}/assets/img/menu.svg"/>
</a>
This is the HTML for the drop down navigation
<div class="header_navigation_mobile">
<ul>
<li>HOME</li>
<li>ABOUT</li>
<li>CONTACT</li>
<li> </li>
<li> </li>
</ul>
</div>
Then my CSS For the Clickable Image to show the navigation
.header_navigation_mobile.is_open{
display: block;
transform: translateY(0%);
}
This is the CSS for the Clickable Image first state which is to Hide it
.header_navigation_mobile{
display: none;
position: absolute;
width: 290px;
background: #484547;
transform: translateY(-100%);
transition: all 0.3s ease-in-out;
}
Then finally my Javascript
function toggleMenu(){
var mobileNav = document.getElementById('mobile_Nav');
var mobileNav_toggle = mobileNav.getAttribute("class");
if(mobileNav_toggle == "header_navigation_mobile") {
mobileNav_toggle == "header_navigation_mobile.is_open";
}
else {
mobileNav_toggle == "header_navigation_mobile";
}
alert(mobileNav_toggle);
}
I would give the menu an ID so it's easier to target, then just toggle a class that you use to hide/show the menu.
.header_navigation_mobile {
display: none;
}
.open {
display: block;
}
toggle
<div class="header_navigation_mobile" id="mobilenav">
<ul>
<li>HOME</li>
<li>ABOUT</li>
<li>CONTACT</li>
<li>
</li>
<li>
</li>
</ul>
</div>
<script>
function toggleMenu(){
var nav = document.getElementById('mobilenav');
nav.classList.toggle('open');
}
</script>
In your JS try like this.use querySelector for selecting elements.And for setting and getting css properties use selector.style.property_name.
function toggleMenu(){
var mobileNav_Hide = document.querySelector('.header_navigation_mobile');
var mobileNav_Show = document.querySelector('.header_navigation_mobile.is_open')
if(mobileNav_Hide.style.display == "none"){
mobileNav_Show.stylr.display == "block";
}
else{
mobileNav.style.display = "none";
}
}
I know this is not a specific answer to your question but may be a handy solution as well. I put together a jsfiddle of a hidden menu you can easily edit to your needs. Here is the link.
#Btn{
position: fixed;
right: 20px;
width: 20px;
height: 24px;
background: linear-gradient(#FFF,#DDD);
border: #AAA 1px solid;
border-radius: 2px;
padding: 2px 5px;
cursor: pointer;
transition: border 0.3s linear 0s;
}
#Btn:hover{
border: #06F 1px solid;
}
#Btn:hover div{
background: #06F;
}
#Btn > div{
width: 20px;
height: 4px;
background: #333;
margin: 3px 0px;
border-radius: 4px;
transition: background 0.3s linear 0s;
}
#hidden{
position: fixed;
right: -300px;
top: 60px;
width: 260px;
height: 0px;
background: #333;
color:#ededed;
padding:15px;
transition: right 0.3s linear 0s;
}
<script>
function toggleBTN(){
var hidden = document.getElementById("hidden");
hidden.style.height = window.innerHeight - 60+"px";
if(hidden.style.right == "0px"){
hidden.style.right = "-300px";
} else {
hidden.style.right = "0px";
}
}
</script>
<div id="Btn" onclick="toggleBTN()">
<div></div>
<div></div>
<div></div>
</div>
<div id="hidden">
<ul>
<li>MENU ITEM 1</li>
<li>MENU ITEM 2</li>
<li>MENU ITEM 3</li>
</ul>
</div>
<div id="page_content">
<script>
for(var i = 0; i < 10; i++){ document.write("<h2>"+(i+1)+". Dummy page content ... </h2>"); }
</script>
</div>
Hope this helps :)
There are a few things in the else statement where is mobileNav coming from there is no instance of that anywhere in your code. Taking a step back a minute from your solution does the adding and removal of the classname is_open define the entire show hide behaviour you desire?
Your process of getting the same element with the subclass present/not present and then trying to set the display property is confusing.
You have an equality comparer when setting the display property when it should be just a single equals which along with my first statement i think is your main problem.
Instead of the way you are doing it i would just look at toggling the is_open class this link Toggle class on HTML element without jQuery shows how to do that and also demonstrates a few other ways of toggling styles including without javascript
Finally just check because im being lazy that display is valid as a property and that it shouldnt instead be style.display as others have indicated
js
let c = document.getElementById("dropdownlist");
let e = document.getElementById("dropdownicon");
let d = e.classList.toggle('fa-angle-down');
if(d===true) {
if(c.style.display==='none'){
c.style.display = 'block';
}
else{
c.style.display ='block';
}
}
else{
c.style.display = 'none';
}

cannot make list item visible with > CSS selector

I'm unable to make the popups 'redItem', 'blueItem' and 'greenItem' below visible again after setting their display to 'none'. I'm using a CSS selector to get them visible again when the mouse hovers over a node higher up in the nested list to no avail.
Here's the code:
<ul class="popups" style="vertical-align: bottom; padding: 0px; margin: 0px;">
<li style="width: 165px"><a id="topmostBox" href="#">One_high-up_item</a>
<ul class="popups">
<li>First-lower-item
<ul class="popups">
<li name="redItem" >Red</li>
<li name="blueItem">Blue</li>
<li name="greenItem">Green</li>
</ul>
</li>
</ul>
</li>
</ul>
.popups:hover > li {
display: block;
}
.popups {
background-color: white;
font-family: sans-serif;
font-size: 13.5px;
list-style: none;
position: relative;
border: 1px solid lightgray;
border-width: .05em;
border-top-color: rgb(165,165,165);
line-height: 1.2em;
display: inline-table;
}
function setTopColorAndVis(theNestedPopupAnchor)
{
var theColorName = theNestedPopupAnchor.innerHTML;
var topMenuBox = document.getElementById('topmostBox');
topMenuBox.innerHTML = theColorName ;
theNestedPopupAnchor.parentNode.style.display = "none";
}
What happens is this:
1) I select the color 'Red' (the 1st list item)
2) my call to setTopColorAndVis(this) makes the popup disappear (because the user selected an item, the color "Red", and now the popup is not needed for now)
3) but when I later hover the mouse over the "First-lower-item" list item, the child li that has the ul containing 'redItem', 'greenItem', 'blueItem' does not appear.
So my experience here is that I'm successfully able to hide the list items named 'redItem', 'blueItem' and 'greenItem' -- but when I hover over the "First-lower-item", despite my CSS code:
.popups:hover > li {
display: block;
}
The 'redItem', 'greenItem' and 'blueItem' do NOT reappear.
What am I missing here?
The inline style overrides you style in your css code. you should use onmouseover event and onmouseout instead.
Try
<li name="redItem" >Red</li>
function show(elem){
elem.parentNode.style.display = "block";
}
function hide(elem){
elem.parentNode.style.display = "none";
}
You cannot :hover over an element with display:none as it has no size...
instead of working with display, you can work with visibility - which will leave an area to hover over.
like so:
theNestedPopupAnchor.parentNode.style.visibility = 'hidden'
.popups:hover > li {
visibility: visible;
}
http://www.w3schools.com/cssref/pr_class_visibility.asp

Categories

Resources