Can't open dropdown menu using querySelectorAll - javascript

I have a mobile navigation with two dropdown menus. Here is the markup of the nav:
<div id="mobile-menu" class="mobile-menu container fixed">
<ul>
<li>Home</li>
<li class="dropdown">
Articles <i class="bi bi-chevron-down"></i>
<ul class="submenu hidden">
<li>Submenu item 1</li>
<li>Submenu item 2</li>
</ul>
</li>
<li>Contact</li>
<li class="dropdown">
My account <i class="bi bi-chevron-down"></i>
<ul class="submenu hidden">
<li>Dashboard</li>
<li>Profile</li>
</ul>
</li>
</ul>
</div>
The dropdown menus should open/expand when clicked. Originally I grabbed the dropdown and submnenu classes like this:
const mobileDropdown = document.querySelector(".dropdown");
const mobileSubMenu = document.querySelector('.submenu');
and used an eventlistener to toggle the "hidden" class which is just a display:none
mobileDropdown.addEventListener('click', () => {
mobileSubMenu.classList.toggle('hidden');
});
The problem with this is that this will only open the first dropdown menu and I cannot open the second.
When I try to use querySelectorAll instead of just querySelector then i get thiserror:
Uncaught typeError addEventListener is not a function
Here I read that with querySelectorAll I need to use a for or foreach loop.
but i think I'm messing it up. I tried this:
const mobileDropdown = document.querySelectorAll(".dropdown");
const mobileSubMenu = document.querySelector('.submenu');
mobileDropdown.forEach(md => md.addEventListener('click', () => {
mobileSubMenu.classList.toggle('hidden');
}));
Now I don't get an error, I can open the first dropdown menu, but when I try to open the second dropdown menu, the first one opens. What am I doing wrong? Thanks in advance.

The problem in your code is that each mobileDropdown's clickEvent was linked to the first submenu, you should link mobileDropdown's clickEvents to their submenu children like that
document.querySelectorAll(".dropdown").forEach(md => md.addEventListener('click', () => {
md.querySelector(".submenu").classList.toggle('hidden');
}));
.hidden {
visibility: hidden;
}
<div class="mobile-menu container fixed" id="mobile-menu">
<ul>
<li>Home</li>
<li class="dropdown">
Articles <i class="bi bi-chevron-down"></i>
<ul class="submenu hidden">
<li>Submenu item 1</li>
<li>Submenu item 2</li>
</ul>
</li>
<li>Contact</li>
<li class="dropdown">
My account <i class="bi bi-chevron-down"></i>
<ul class="submenu hidden">
<li>Dashboard</li>
<li>Profile</li>
</ul>
</li>
</ul>
</div>

here is a solution. You have to listen to the 'click' event for "submenu" on the element of "dropdown".
const mobileDropdown = document.querySelectorAll(".dropdown");
mobileDropdown.forEach((md) => {
md.addEventListener("click", () => {
const mobileSubMenu = md.querySelector(".submenu");
mobileSubMenu.classList.toggle("hidden");
});
});

mobileSubMenu always the first!

Related

Nested dropdrown menu

I'm trying to build a nested dropdown menu with javascript and click event. My html is this:
<ul>
<li id="menu-item-439" class="dropdown">
hostelerĂ­a
<ul class="sub-menu">
<li id="menu-item-459">
Comanderos
</li>
<li id="menu-item-457" class="dropdown">
Venta online
<ul class="sub-menu">
<li>
PortalRest Pedidos
</li>
</ul>
</li>
</ul>
</li>
</ul>
and my javascript:
document.querySelectorAll('.dropdown').forEach(i => {
i.addEventListener('click', (e) => {
i.classList.toggle('show');
})
});
The problem is in the nested "dropdown" class. When i click in "child dropdown" (id="menu-item-457") toogle not only affects that element but also affects the parent (id="menu-item-439")
Thanks!!
Here
HTML
<ul>
<li id="menu-item-439" class="dropdown">
hostelerĂ­a
<ul class="sub-menu">
<li id="menu-item-459">
Comanderos
</li>
<li id="menu-item-457" class="dropdown">
Venta online
<ul class="sub-menu">
<li>
PortalRest Pedidos
</li>
</ul>
</li>
</ul>
</li>
</ul>
JS
let n=0, n2=0;
function toggle() {
n++;
if (n%2==0) {
document.getElementsByClassName("sub-menu")[0].style.display="none";
}
else {
document.getElementsByClassName("sub-menu")[0].style.display="block";
}
}
function toggle2() {
n2++;
if (n2%2==0) {
document.getElementsByClassName("sub-menu")[1].style.display="none";
}
else {
document.getElementsByClassName("sub-menu")[1].style.display="block";
}
}
CSS
.sub-menu {display: none}
If you wanted same with JQuery toggle method, then js should be like this
JS
function toggle() {
document.getElementsByClassName("sub-menu")[0].toggle();
}
function toggle2() {
document.getElementsByClassName("sub-menu")[1].toggle();
}

Add class to element on hover in Vue, without using data

In a Vue-component, I have a menu like this:
<ul class="menu-outer-wrapper">
<li>Foo 1</li>
<li class="has-children">
Foo 2
<ul>
<li>Child 1</li>
<li>Child 2</li>
<li>Child 3</li>
</ul>
</li>
<li>Foo 5</li>
<li class="has-children">
Foo 6
<ul>
<li>Child 1</li>
<li>Child 2</li>
</ul>
</li>
<li>Foo 7</li>
<li>Foo 8</li>
</ul>
And I would like to add the class hovered to the li.has-children-elements upon hover (mouseenter) (to be able to make some nicer animations for the children of that dropdown. And remove that class on mouseleave.
I know that there are options to do this with pure CSS, - but controlling delays and soft fade-in's are a pain (and become very messy very fast, without adding some classes).
I imagined doing something like this:
...
mounted(){
let liWithChildren = document.querySelectorAll( '.menu-outer-wrapper > li.has-children' );
liWithChildren.forEach( (event, window) => {
// Somehow add the class to the hovered element here.
// But I don't know how. Or if it's a good/bad idea (performance-wise).
}
}
But is that the way to go? And can I do it without using data (since the menu is dynamically generated by a CMS-system.
Update 1
I'm trying to keep my markdown readable. So I would like to avoid something like this:
<ul class="menu-outer-wrapper">
<li :class="[ { 'hovered' : someVar } ]">
Foo 1
</li>
<li :class="[ { 'hovered' : someVar }, 'has-children' ]">
Foo 2
<ul>
<li>Child 1</li>
<li>Child 2</li>
<li>Child 3</li>
</ul>
</li>
<li :class="[ { 'hovered' : someVar } ]">
Foo 2
</li>
...
...
...
Both since it won't gel with the dynamically generated menu.
And also since it add a lot of noise to the markdown.
Update 2
I simplified the example, to make it easier to digest. But due to the comments I figured I would elaborate on the dynamic generated menu. I'm making it something like this:
<nav id="secondary-menu" v-if="secondaryMenu">
<ul>
<li
:class="[ { 'has-children': r.children } ]"
v-for="(r, r_key, r_index) in secondaryMenu">
<a :href="r.url" :title="r.title">
{{ r.title }}
</a>
<ul class="children" v-if="r.children">
<li v-for="(c1, c1_key, c1_index) in r.children">
<a :href="c1.url" :title="c1.title">
{{ c1.title }}
</a>
</li>
</ul>
</li>
</ul>
</nav>
You just need the #mouseenter and #mouseleave events. All you need to do is listen for the appropriate events on all list-items that could have children, then perform your class addition (or removal) if the target element has the class of "has-children". Here's how I would do it:
<template>
<nav id="secondary-menu" v-if="secondaryMenu">
<ul>
<li
:class="[{ 'has-children': r.children }]"
v-for="(r, r_key, r_index) in secondaryMenu"
:key="`${r_key}-${r_index}`"
#mouseenter="addClass"
#mouseleave="removeClass"
>
<a :href="r.url" :title="r.title">
{{ r.title }}
</a>
<ul class="children" v-if="r.children">
<li
v-for="(c1, c1_key, c1_index) in r.children"
:key="`${c1_key}-${c1_index}`"
>
<a :href="c1.url" :title="c1.title">
{{ c1.title }}
</a>
</li>
</ul>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: "HoverNav",
props: {
secondaryMenu: {
type: Array,
required: true,
},
},
methods: {
addClass: function (e) {
if (e.target.classList.contains("has-children")) {
e.target.classList.add("hovered");
}
},
removeClass: function (e) {
if (e.target.classList.contains("has-children")) {
e.target.classList.remove("hovered");
}
},
},
};
</script>
Here's a very unaesthetic sandbox of this in action. Let me know if this works for you :)
https://codesandbox.io/s/headless-brook-ysq97?file=/src/components/HoverNav.vue:0-1169

Make parent menu link toggle submenu link

I have a menu which I have working fine but I need a small change and that is I need the parent menu to toggle the submenu, currently if you click the parent menu the submenu appears but I need it so when you click the parent menu again it closes the sub menu.
You can see the menu in action here.
and this is the javascript that is for the menu:
$('.dropdown-toggle').click(function() {
$('.dropdown').css('display', 'none'); // Hide submenus when other submenus are clicked
$(this).next('.dropdown').toggle();
});
$(document).click(function(e) {
var target = e.target;
if (!$(target).is('.dropdown-toggle') && !$(target).parents().is('.dropdown-toggle')) {
$('.dropdown').hide();
}
});
This is the menu html
<nav class="main">
<a class="dropdown-toggle" href="#" title="Menu">Menu One</a>
<ul class="dropdown">
<li>Menu Item</li>
<li>Menu</li>
<li>Settings</li>
<li>Search</li>
</ul>
</nav>
Replace this code:
$('.dropdown-toggle').click(function() {
$('.dropdown').css('display', 'none'); // Hide submenus when other submenus are clicked
$(this).next('.dropdown').toggle();
});
With the code below:
$('.dropdown-toggle').click(function() {
var $currentDropdown = $(this).next('.dropdown');
$currentDropdown.siblings('.dropdown').not($currentDropdown).removeClass('toggled');
$currentDropdown.siblings('.dropdown').not($currentDropdown).hide();
$currentDropdown.toggleClass('toggled');
$currentDropdown.toggle();
});
That should do it.
I think this is the simplier way :)
Hope you helped
$(".dropdown").css('display', 'none');
$('.dropdown-toggle').click(function(e) {
if ($(this).next().is(":visible")){
$(this).next().hide();
}else{
$(".dropdown").hide();
$(this).next().show();
}
});
a {
display: block;
}
<script src="https://code.jquery.com/jquery-3.1.0.js"></script><nav class="main">
<nav>
<a class="dropdown-toggle" href="#" title="Menu">Menu One</a>
<ul class="dropdown">
<li>Menu Item</li>
<li>Menu</li>
<li>Settings</li>
<li>Search</li>
</ul>
<a class="dropdown-toggle" href="#" title="Menu">Menu One</a>
<ul class="dropdown">
<li>Menu Item</li>
<li>Menu</li>
<li>Settings</li>
<li>Search</li>
</ul>
</nav>
I think this code will be enough.
$('.dropdown-toggle').click(function() {
$(this).next('.dropdown').toggle();
});
Codepen Example
This can be done easily with Bootstrap:
<li class="submenu">
<i class="la la-user"></i>
<span> Main menue </span>
<span class="menu-arrow"></span>
<ul style="display: none;">
<li>sub menu 1</li>
<li> sub menu 2 </li>
<li> sub menu 3 </li>
</ul>
</li>

How to refactor this JQuery code so that it follows the DRY principle

See. I have the following html <ul>-<li> lists here.
<ul class="nav">
<li class="active">Home</li>
<li class="dropdown">
About Us<b class="caret"></b>
<ul class="dropdown-menu">
<li>Who we are?</li>
<li>What we stand for?</li>
</ul>
</li>
<li class="dropdown">
Campaigns<b class="caret"></b>
<ul class="dropdown-menu">
<li>Get Involved</li>
</ul>
</li>
<li>News</li>
<li>Donate</li>
</ul>
Then I have the following jquery code
$("#about-us").click(function(){
$("ul.nav").children("li").children("a").css("background-color", "#0E0E0E");
$(this).css("background-color","#47F514");
});
$("#campaigns").click(function(){
$("ul.nav").children("li").children("a").css("background-color", "#0E0E0E");
$(this).css("background-color","#F2720A");
});
$("#news").click(function(){
$("ul.nav").children("li").children("a").css("background-color", "#0E0E0E");
$(this).css("background-color","#0A76F2");
});
$("#donate").click(function(){
$("ul.nav").children("li").children("a").css("background-color", "#0E0E0E");
$(this).css("background-color","#F7A116");
});
I can see that line $("ul.nav").children("li").children("a").css("background-color", "#0E0E0E") is repeated in many lines. So surely I can take this out and put a new function in each click events to make this same calls this many times.
What should I do to refactor this?
You can do it this way :
var colors_array_by_id = { "about-us" : "#47F514", "campaigns" : "#F2720A", "news" : "#0A76F2", "donate" : "#F7A116" };
$("#about-us, #campaigns, #news, #donate").click(function(){
$("ul.nav > li > a").css("background-color", "#0E0E0E");
$(this).css("background-color", colors_array_by_id[$(this).attr('id')]);
});
The only thing that changes is the color depending of the clicked element. So, I created here an associative array containing id-color couples.
Here's how I would do it -
<ul class="nav">
<li class="active">Home</li>
<li class="dropdown">
About Us<b class="caret"></b>
<ul class="dropdown-menu">
<li>Who we are?</li>
<li>What we stand for?</li>
</ul>
</li>
<li class="dropdown">
Campaigns<b class="caret"></b>
<ul class="dropdown-menu">
<li>Get Involved</li>
</ul>
</li>
<li><a href="#" id="news" class="navLinks" data-bgcolor="#0A76F2" >News</a></li>
<li><a href="#" id="donate" class="navLinks" data-bgcolor="#F7A116" >Donate</a></li>
</ul>
I've added a data attribute to the links called data-bgcolor which has the color value. Next I attach a single click handler to all the links.
$("#about-us, #campaigns, #news, #donate").click(function(){
$("ul.nav").children("li").children("a").css("background-color", "#0E0E0E");
$(this).css("background-color", $(this).attr("data-bgcolor"));
});
You could do this,
In css,
.myClass{ background-color: #OEOEOE;}
on load,
var elm = $("ul.nav").children("li").children("a");
and then,
$("#about-us, #campaigns, #news, #donate").click(function(){
$this = $(this);
elm.addClass("myClass");
$this.css("background-color", $this.data("bgcolor"));
});
You can use the .data() instead of .attr().

click blank area go back to selected section

<li class="list ">A
<ul class="names">
<li class="list">1
</li>
<li class="list">2
</li>
</ul>
</li>
<li class="list ">B
<ul class="names selected">
<li class="list selected">1
</li>
<li class="list">2
</li>
<li class="list">3
</li>
<li class="list">4
</li>
</ul>
</li>
<li class="list ">C
<ul class="names">
<li class="list">1
</li>
<li class="list">2
</li>
<li class="list">3
</li>
<li class="list">4
</li>
</ul>
</li>
$('.list').click(function () {
var that = this;
$('.list').each(function () {
if (that == this) return true; //continue
$('.names:not(:hidden)', this).slideToggle();
});
$('ul.names', this).slideToggle();
})
ul.names{display: none;}
li.list{
width:150px;
background:#A9FF7A;
}
ul.names {
width:150px;
background:#A9FF7A;
}
ul.selected{
display: block;
}
li.selected{
background:red;
}
online Sample: http://jsfiddle.net/gyYyd/
B's submenu 1 is highlighted. If I click on menu A or C, then A or C section will be opened, but how do I click PAGE BLANK area (outside of the background color) to go back to B section (to open B section)
Thanks in advance
You can capture clicks on the document object and trigger a click on the required list item.
$(document).click(function() {
var selected = $('.selected:first');
if(!selected.closest('ul.names').is(':visible')) {
selected.closest('.list').trigger('click');
}
});
Also, make sure to return false from your current list item click handler - so that normal clicks on list items don't propagate to the above handler.
DEMO: http://jsfiddle.net/gyYyd/2/

Categories

Resources