I have an expandable menu with nested submenus to an infinite depth.
<ul>
<li>One</li>
<li class="contains-submenu">Two
<ul>
<li>Two A</li>
<li class="contains-submenu">Two B
<ul>
<li>Two B I</li>
<li>Two B II</li>
<li>Two B III</li>
</ul>
</li>
<li>Two C</li>
</ul>
</li>
<li>Three</li>
<li class="contains-submenu">Four
<ul>
<li>Four A</li>
<li class="contains-submenu">Four B
<ul>
<li>Four B I</li>
<li>Four B II</li>
<li>Four B III</li>
</ul>
</li>
<li>Four C</li>
</ul>
</li>
</ul>
I want to show / hide any sub-menu at any level, without influencing the show / hide status of any other sub-menu and especially not the show / hide status of any ancestor of the sub-menu.
What I found initially was that if I used a click event listener to toggle a class .show-submenu to show or hide a submenu, when a submenu lower down (e.g. Two B) was closed, then its parent, the submenu Two, would also close.
It became apparent that this was happening because at almost the same moment I was clicking on Two B, a click event was also registering on Two (...of course it was - Two B is nested inside Two - any click on Two B will also register as a click on Two).
I tried to fix this using .stopPropagation()... but then I realised that actually this wasn't (at least I don't think it was) a case of a single event registering on two different elements as the event bubbled up through Two from Two B, but two different events registering, one on Two almost immediately after the other on Two B.
My solution was the following:
var activateToggle = true;
function toggleSubmenu() {
if (activateToggle === true) {
this.classList.toggle('show-submenu');
activateToggle = false;
}
setTimeout(function(){activateToggle = true}, 100);
}
Is this a reasonable approach to produce the desired effect?
Or is there a more conventional approach using event.target or similar?
Complete Working Example:
var querySelector = 'ul > li > ul';
var selectedElements = [... document.querySelectorAll(querySelector)];
var activateToggle = true;
var initialPadding = 6;
var subsequentPadding = 18;
var level = 0;
while (selectedElements.length > 0) {
for (var j = 0; j < selectedElements.length; j++) {
selectedElements[j].parentNode.classList.add('contains-submenu');
selectedElements[j].style.marginLeft = (0 - (initialPadding + (level * subsequentPadding))) + 'px';
[... selectedElements[j].children].forEach(function(childElement){
childElement.style.paddingLeft = (initialPadding + ((level + 1) * subsequentPadding)) + 'px';
});
}
level++;
querySelector += ' > li > ul';
selectedElements = [... document.querySelectorAll(querySelector)];
}
var containsSubmenus = document.getElementsByClassName('contains-submenu');
function toggleSubmenu() {
if (activateToggle === true) {
this.classList.toggle('show-submenu');
activateToggle = false;
}
setTimeout(function(){activateToggle = true}, 100);
}
for (let i = 0; i < containsSubmenus.length; i++) {
containsSubmenus[i].addEventListener('click', toggleSubmenu, false);
}
ul {
margin-left: 0;
padding-left: 0;
font-family: arial, helvetica, sans-serif;
color: rgb(255, 255, 255);
background-color: rgba(0, 75, 165, 1);
list-style-type: none;
}
li {
padding-left: 6px;
font-size: 16px;
line-height: 32px;
}
ul ul {
display: none;
}
.show-submenu > ul {
display: block;
}
li.show-submenu {
height: auto;
}
.contains-submenu {
font-weight: 700;
cursor: pointer;
}
.contains-submenu ul {
font-weight: 300;
cursor: default;
}
.contains-submenu,
.contains-submenu ul {
background-color: rgba(255, 255, 255, 0.1);
}
<ul>
<li>One</li>
<li class="contains-submenu">Two
<ul>
<li>Two A</li>
<li class="contains-submenu">Two B
<ul>
<li>Two B I</li>
<li>Two B II</li>
<li>Two B III</li>
</ul>
</li>
<li>Two C</li>
</ul>
</li>
<li>Three</li>
<li class="contains-submenu">Four
<ul>
<li>Four A</li>
<li class="contains-submenu">Four B
<ul>
<li>Four B I</li>
<li>Four B II</li>
<li>Four B III</li>
</ul>
</li>
<li>Four C</li>
</ul>
</li>
</ul>