简体   繁体   中英

Vanilla Javascript Toggle Drop Down Menu

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>

Toggling menu visibility

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 (ie 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>


A variant with ES6 syntax

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>


Toggling menu visibility + closing when clicking elsewhere

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>

It's hard to compete with Ivan's answer but this would be my solution to the same problem using ES6 syntax:

class Dropdown {

  constructor() {
    this.listen();
  }

  // Listen to ALL (!) click events to also catch clicks OUTSIDE the dropdowns
  listen() {
    document.addEventListener('click', (e) => {
      if (e.target.classList.contains('dropdown')) {
        this.closeOthers(e.target);
        this.handleClick(e.target);
      } else {
        this.closeOthers(null);
      }
    });
  }

  // Add or remove 'expanded' CSS class, depending on the current situation
  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
  closeOthers(element) {
    document.querySelectorAll('.dropdown').forEach((dropdown) => {
      if (element != dropdown) {
        dropdown.classList.remove('expanded');
      }
    });
  }

}

document.addEventListener('DOMContentLoaded', () => new Dropdown);

It works for me. Not sure if it can work for someone else. Feedback appreciated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM