I have some vanilla JavaScript for a website menu which works perfectly (thanks mainly to Titus on Stack Overflow), opening and closing a sub-menu (toggling <ul class="hidden">
) if the UL's sibling button is clicked or closing the sub-menu if another button on the same level as the sibling is clicked, adjusting other CSS classes for the <li>
elements appropriately. The code is specific to each level of the menu and I have repeated the code for several more menu levels, just changing each occurrence of '1' to '2' and then to '3' and so on (potentially there could be up to 10 levels; I already have 5 for testing purposes). Is there a way to use less lines of code whilst maintaining each level's independence?
Here is the code for two levels:
const buttons1 = Array.from(document.querySelectorAll(".btn-level-1"));
buttons1.forEach((button1) => {
button1.addEventListener('click', () => {
buttons1.filter(b1 => b1 != button1).forEach(b1 => {
b1.classList.remove('opened');
b1.classList.add('not-open');
b1.nextElementSibling.classList.add('hidden')
});
button1.nextElementSibling.classList.toggle('hidden');
button1.classList.toggle('not-open');
button1.classList.toggle('opened');
});
});
const buttons2 = Array.from(document.querySelectorAll(".btn-level-2"));
buttons2.forEach((button2) => {
button2.addEventListener('click', () => {
buttons2.filter(b2 => b2 != button2).forEach(b2 => {
b2.classList.remove('opened');
b2.classList.add('not-open');
b2.nextElementSibling.classList.add('hidden')
});
button2.nextElementSibling.classList.toggle('hidden');
button2.classList.toggle('not-open');
button2.classList.toggle('opened');
});
});
Demo HTML, CSS and another bit of JS in a snippet:
document.addEventListener('DOMContentLoaded', () => {
document.getElementById("mainbody").addEventListener("click", closeMenu);
function closeMenu(event) {
if(event.target.type != "button") {
let level1 = document.getElementsByClassName("level-1");
let level2 = document.getElementsByClassName("level-2");
let btnlevel1 = document.getElementsByClassName("btn-level-1");
let btnlevel2 = document.getElementsByClassName("btn-level-2");
for (let i = 0; i < level1.length; i++) {
level1[i].classList.add("hidden"); }
for (let i = 0; i < level2.length; i++) {
level2[i].classList.add("hidden"); }
for (let i = 0; i < btnlevel1.length; i++){
btnlevel1[i].classList.remove("opened");}
for (let i = 0; i < btnlevel2.length; i++) {
btnlevel2[i].classList.remove("opened");}
}
}
const buttons1 = Array.from(document.querySelectorAll(".btn-level-1"));
buttons1.forEach((button1) => {
button1.addEventListener('click', () => {
buttons1.filter(b1 => b1 != button1).forEach(b1 => {
b1.classList.remove('opened');
b1.nextElementSibling.classList.add('hidden')
});
button1.nextElementSibling.classList.toggle('hidden');
button1.classList.toggle('opened');
});
});
const buttons2 = Array.from(document.querySelectorAll(".btn-level-2"));
buttons2.forEach((button2) => {
button2.addEventListener('click', () => {
buttons2.filter(b2 => b2 != button2).forEach(b2 => {
b2.classList.remove('opened');
b2.nextElementSibling.classList.add('hidden')
});
button2.nextElementSibling.classList.toggle('hidden');
button2.classList.toggle('opened');
});
});
})
.sticky-main-menu {
position: fixed;
top: 6rem;
left: 0;
right: 0;
z-index: 9999;
}
.stickymenuwrapper {
background-color: #fff;
transition-duration: 0s;
padding: 1rem;
margin: 0 auto;
z-index: 995;
border: 1px solid gray;
}
.hmenu .hidden {
display: none;
}
.hmenu ul {
list-style: none;
margin-bottom: 0;
white-space: nowrap;
padding: 0;
}
.hmenu ul.navbar {
background-color: #f7a1d6;
padding: 0 1rem;
justify-content: center;
display: flex; /* required for VSC */
/* text-align: center;*/
}
.hmenu .navbar {
line-height: 1.5;
}
.hmenu li ul {
position: absolute;
top: 100%;
background-color: pink;
padding: 0 12px 12px;
}
.hmenu ul[class^="level"] {
background-color: white;
position: relative;
top: 100%;
left: 0;
padding: 1rem 0 1rem 1rem;
margin-top: 0.5rem;
}
.hmenu ul.level-1 {
position: absolute; /* ? */
top: 100%;
left: 0;
margin-top: 0;
padding: 1rem;
box-shadow: 0 2px 5px #acaaaa;
}
.hmenu ul.level-2,
.hmenu ul.level-4{
background-color: #dedede;
}
.hmenu ul li:last-child {
padding-bottom: 0;
}
.hmenu li.active>a {
text-decoration: underline;
}
.hmenu a.separator:hover {
text-decoration: none;
}
.hmenu ul[class^="level"] li {
margin-right: 1rem;
}
.hmenu li {
position: relative;
/* display: flex;*/
margin: 0;
padding: 0 0 0.5rem;
line-height: 1.5rem;
}
.hmenu li a {
color: #0d6efd;
}
.hmenu li.parent {
margin-right: 0; /* was 2rem */
}
.hmenu .navbar>li {
padding: 0;
margin: 0 0.5rem;
display: flex;
}
.hmenu .navbar>li.parent {
margin-right: 2rem;
}
.hmenu .navbar>li.divider.parent {
margin-right: 0;
}
.hmenu .navbar>li>a {
padding: 0.5rem 0 0.5rem 0.5rem;
margin: 0 0 0 0;
}
.hmenu .navbar>li:first-child>a {
padding: 0.5rem 0.5rem 0.5rem 0;
margin-left: 0;
}
.hmenu li.heading>a,
.hmenu li.divider>a,
.hmenu li.separator>a {
text-decoration: none;
padding-right: 2rem;
}
.hmenu li.separator-line {
position: relative;
height: 1rem;
display: inline-block;
width: 100%;
}
.hmenu li.separator-line span {
display: none;
}
.hmenu li.separator-line:after {
position: absolute;
height: 0;
top: 60%;
width: 100%;
border-top: 1px solid#8c8a8a;
border-bottom: 1px solid lightgray;
margin: 0 0;
content: "";
}
.hmenu ul.navbar>li a {
padding-top: 0.5rem;
position: relative;
}
[type="button"] {
-webkit-appearance: button;
}
[type="button"]:not(:disabled) {
cursor: pointer;
}
.hmenu button,
.hmenu button:focus {
display: inline-flex;
position: absolute;
background: transparent;
border: none;
color: #0d6efd;
height: 1.5rem;
outline: none;
padding: 0 0 0 .5rem;
margin: 0;
right: 0;
width: 40px;
top: 6px;
}
.hmenu li.linked button {
position: relative;
top: 0;
}
.hmenu li.linked button.btn-level-1 {
top: 10px;
}
.hmenu .navbar > li.linked.top-level {
margin-right: 0.5rem;
}
.hmenu button.btn-level-1 {
right: 0;
position: absolute;
top: 0;
}
.hmenu li button[class^="btn-level"]:after,
.hmenu li a[class^="btn-level"]:after{
display: inline-flex;
width: 0;
height: 0;
margin: 0 .5em;
content: "";
transition: all .3s ease-out;
position: absolute;
top: 12px;
cursor: pointer;
right: -2rem;
border-top: 10px solid;
border-right: 8px solid transparent;
border-left: 8px solid transparent;
}
.hmenu li.separator>button[class^="btn-level"]:after,
.hmenu li.separator>a[class^="btn-level"]:after{
right: 0;
}
.hmenu li button[class^="btn-level"].opened,
.hmenu li a[class^="btn-level"].opened{
color: red;
}
.hmenu li button[class^="btn-level"].opened:after,
.hmenu li a[class^="btn-level"].opened:after{
border-top: none;
border-bottom: 10px solid;
border-right: 8px solid transparent;
border-left: 8px solid transparent;
top: 12px;
right: 0;
}
.hmenu li.linked > a[class^="btn-level"].opened:after {
right: -2rem;
}
.hmenu li button.linked {
display: inline-flex;
position: relative;
}
.hmenu li button.linked:after{
top: 10px;
right: 0;
}
.hmenu li button[class^="btn-level"].linked.opened:after{
top: 6px;
right: 0;
}
.hmenu li button.btn-level-1.linked {
display: inline-flex;
position: relative;
}
.hmenu li button.btn-level-1:after,
.hmenu li button.btn-level-1.opened:after {
top: 30%;
right: 0;
}
.hmenu li a.btn-level-1:after,
.hmenu li a.btn-level-1.opened:after{
top: 40%;
right: -2rem;
}
<!DOCTYPE html>
<html lang="en-gb"dir="ltr">
<head>
<meta charSet="utf-8" />
<link rel="stylesheet" href="menu-demo-2a.css">
<script type="text/javascript">
// Fix for Firefox autofocus CSS bug
// See: http://stackoverflow.com/questions/18943276/html-5-autofocus-messes-up-css-loading/18945951#18945951
</script>
</head>
<body id="mainbody" class="outer">
<div id="stickymenuwrapper" class="stickymenuwrapper"><p>Click a triangle to open the sub-menu. Click it again to close the sub-menu or click the other triangle to open its menu and close the first. Clicking a triangle on the next level down will leave the top level open, but its siblings work in the same way.
Clicking outside the menu but inside this box will also close the menu. The CSS for this layout is not finished and is not intended for narrow screens. I'm aware that the second triangle will overflow the pink background on a very narrow view. Please ignore that. </p>
<div class="hmenu">
<ul id="menucontent" class="navbar">
<li class="item-103 deeper parent linked">
<a href="/" >ITEM 1</a>
<button class="btn-level-1 linked" type="button" name="btn-103"></button>
<ul class="level-1 hidden not-separator">
<li class="item-104">
<a href="/" >About us</a>
</li>
<li class="item-107 divider deeper parent separator">
<a class="btn-level-2" type="button" >Level 2 separator</a>
<ul class="level-2 hidden separator">
<li class="item-108">
<a href="/" >Lower article</a>
</li>
</ul>
</li>
<li class="item-141 deeper parent linked">
<a class="btn-level-2 heading" type="button" >Menu heading</a>
<ul class="level-2 hidden menu-heading">
<li class="item-142">
<a href="/" >Article under menu heading</a>
</li>
</ul>
</li>
</ul>
</li>
<li class="item-122 divider deeper parent">
<a class="btn-level-1" type="button">ITEM TWO</a>
<ul class="level-1 hidden separator">
<li class="item-121">
<a href="/" >Test level 2</a>
</li>
<li class="item-134 deeper parent linked">
<a class="btn-level-2 heading" type="button" >Test Two menu heading</a>
<ul class="level-2 hidden menu-heading">
<li class="item-135">
<a href="/" >Article under menu heading</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Duis ac lorem. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Suspendisse potenti. Sed tincidunt varius arcu.</p>
</div>
</div>
<script src="menu-demo-2a.js"></script>
</body>
</html>