Reputation: 822
My brain has checked out for weekend...
I am looking for a solution in plain Javascript where if one dropdown menu box is opened on click of another main menu item, the previous opened dropdown would close and then display the newly clicked main menu item's dropdown. I know this is probably so simple, but I cannot come up with a solution that is not convoluted.
Also if you click outside of the menu items (anywhere on the document that is not a menu item or dropdown box) should close any open dropdowns.
Thank you for any help.
function testFunc(el) {
var parent = el.parentElement;
var dd = parent.lastChild.previousSibling;
dd.classList.toggle('show');
}
ul { list-style: none; margin: 0; padding: 0; }
ul li {
width: 100px;
float: left;
background: #dbdbdb;
line-height: 2em;
text-align: center;
margin: 0 5px;
cursor: pointer;
}
ul li span {
display: block;
}
ul li ul {
display: none;
}
.show {
display: block;
}
<ul>
<li>
<span onclick="testFunc(this)">Item 1</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span onclick="testFunc(this)">Item 2</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span onclick="testFunc(this)">Item 3</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span onclick="testFunc(this)">Item 4</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
</ul>
Upvotes: 4
Views: 6809
Reputation: 10207
It's hard to compete with Ivan's answer but this would be my solution:
function Dropdown() {
// Listen to ALL (!) click events to also catch clicks OUTSIDE the dropdowns
document.addEventListener('click', function(e) {
if (e.target.closest('.dropdown')) {
closeOthers(e.target);
handleClick(e.target);
} else {
closeOthers(null);
}
});
// Add or remove 'expanded' CSS class, depending on the current situation
function handleClick(dropdown) {
if (dropdown.classList.contains('expanded')) {
dropdown.classList.remove('expanded');
} else {
dropdown.classList.add('expanded');
}
}
// Close all dropdowns except the one that gets passed as the element parameter
// Note that we may also pass null in order to close ALL dropdowns
function closeOthers(element) {
document.querySelectorAll('.dropdown > a').forEach(link => {
if (element != link) {
link.classList.remove('expanded');
}
});
}
}
document.addEventListener('DOMContentLoaded', Dropdown);
<div class="dropdown">
<a aria-label="Settings"></a>
<ul>
<li><a href="/account">Account</a></li>
<li><a href="/profile">Profile</a></li>
<li><a href="/tutorial">Tutorial</a></li>
</ul>
</div>
It works for me. Not sure if it can work for someone else. Feedback appreciated.
Upvotes: 2
Reputation: 40648
You can save the last opened menu in a variable opened
outside the function. Then when a menu is clicked if opened
is not null
it will toggle the opened
(i.e. hide the last opened menu) and toggle the clicked item.
let opened = null
function testFunc(el) {
// gets the <ul> element of the clicked menu item
const menu = el.parentElement.lastChild.previousSibling;
if (!opened) {
// no menu item is shown
opened = menu
opened.classList.toggle('show');
} else if (menu == opened) {
// the clicked item is already showing
menu.classList.toggle('show')
opened = null
} else {
// the clicked item is hiddden but another one is showing
opened.classList.toggle('show')
opened = menu
opened.classList.toggle('show')
}
}
Here is the code:
let opened = null
function testFunc(el) {
const menu = el.parentElement.lastChild.previousSibling;
if(!opened) {
opened = menu
opened.classList.toggle('show');
} else if(menu == opened) {
menu.classList.toggle('show')
opened = null
} else {
opened.classList.toggle('show')
opened = menu
opened.classList.toggle('show')
}
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
ul li {
width: 100px;
float: left;
background: #dbdbdb;
line-height: 2em;
text-align: center;
margin: 0 5px;
cursor: pointer;
}
ul li span {
display: block;
}
ul li ul {
display: none;
}
.show {
display: block;
}
<ul>
<li>
<span onclick="testFunc(this)">Item 1</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span onclick="testFunc(this)">Item 2</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span onclick="testFunc(this)">Item 3</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span onclick="testFunc(this)">Item 4</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
</ul>
Here is a variant with some ES6 syntax, note I have changed the HTML naming structure to better maintain the code, calling the elements by class name allows
to not have to use of inline event listeners
call all the menu items in one line
Here is the JavaScript code:
let opened = null
const toggleVisibility = e => e.classList.toggle('show')
const toggleDropDown = e => {
const clickedItem = e.target.parentElement.lastChild.previousSibling
toggleVisibility(clickedItem);
if (!opened) {
opened = clickedItem
} else if (opened == clickedItem) {
opened = null
} else {
toggleVisibility(opened);
opened = clickedItem
}
}
[...document.querySelectorAll('.dropDown')].forEach(dropDown => dropDown.addEventListener('click', toggleDropDown))
let opened = null
const toggleVisibility = e => e.classList.toggle('show')
const toggleDropDown = e => {
const clickedItem = e.target.parentElement.lastChild.previousSibling
toggleVisibility(clickedItem);
if (!opened) {
opened = clickedItem
} else if (opened == clickedItem) {
opened = null
} else {
toggleVisibility(opened);
opened = clickedItem
}
}
[...document.querySelectorAll('.dropDown')].forEach(dropDown => dropDown.addEventListener('click', toggleDropDown))
ul {
list-style: none;
margin: 0;
padding: 0;
}
ul li {
width: 100px;
float: left;
background: #dbdbdb;
line-height: 2em;
text-align: center;
margin: 0 5px;
cursor: pointer;
}
ul li span {
display: block;
}
ul li ul {
display: none;
}
.show {
display: block;
}
<ul>
<li>
<span class="dropDown">Item 1</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span class="dropDown">Item 2</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span class="dropDown">Item 3</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span class="dropDown">Item 4</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
</ul>
If you want to close any opened menu if the user clicks outside of the menu you'll need to have an event listener on the document itself. So instead of having one event listener per menu button, you will have a single one watching for any click happening in the document.
The event listener will determine if the clicked item is a menu button, in this case, it will run the menu handler. Else it will close the last opened menu item.
JavaScript code:
let opened = null
const toggleVisibility = e => e.classList.toggle('show')
const handleDropdown = e => {
const clickedItem = e.parentElement.lastChild.previousSibling
toggleVisibility(clickedItem)
if (!opened) {
opened = clickedItem
} else if (opened == clickedItem) {
opened = null
} else {
toggleVisibility(opened)
opened = clickedItem
}
}
const handleClick = e => {
if (e.target.className.includes('dropDown')) {
handleDropdown(e.target)
} else if (opened) {
toggleVisibility(opened)
opened = null
}
}
document.addEventListener('click', handleClick)
Here is the full code:
let opened = null
const toggleVisibility = e => e.classList.toggle('show')
const handleDropdown = e => {
const clickedItem = e.parentElement.lastChild.previousSibling
toggleVisibility(clickedItem)
if (!opened) {
opened = clickedItem
} else if (opened == clickedItem) {
opened = null
} else {
toggleVisibility(opened)
opened = clickedItem
}
}
const handleClick = e => {
if (e.target.className.includes('dropDown')) {
handleDropdown(e.target)
} else if (opened) {
toggleVisibility(opened)
opened = null
}
}
document.addEventListener('click', handleClick)
ul {
list-style: none;
margin: 0;
padding: 0;
}
ul li {
width: 100px;
float: left;
background: #dbdbdb;
line-height: 2em;
text-align: center;
margin: 0 5px;
cursor: pointer;
}
ul li span {
display: block;
}
ul li ul {
display: none;
}
.show {
display: block;
}
<ul>
<li>
<span class="dropDown">Item 1</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span class="dropDown">Item 2</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span class="dropDown">Item 3</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
<li>
<span class="dropDown">Item 4</span>
<ul>
<li>Sub Item 1</li>
<li>Sub Item 2</li>
</ul>
</li>
</ul>
Upvotes: 7