纯JavaScript下拉菜单切换

3

我的大脑已经开始周末模式...

我正在寻找一个纯JavaScript的解决方案,其中如果单击其他主菜单项上的下拉菜单框,则先前打开的下拉菜单将关闭,然后显示新点击的主菜单项的下拉菜单。我知道这可能很简单,但是我无法想出不复杂的解决方案。

此外,如果您在菜单项之外(即不是菜单项或下拉框的文档的任何位置)单击,则应关闭所有打开的下拉菜单。

感谢您的帮助。

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>

2个回答

6

切换菜单可见性

您可以将最后打开的菜单保存在函数外的变量opened中。然后,当单击菜单时,如果opened不为null,它将切换opened(即隐藏上一个打开的菜单)并切换单击的项目。

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')

  }

}

这是代码:

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>

ES6语法变体

这里是一个带有一些ES6语法的变体,注意我已经更改了HTML命名结构以更好地维护代码,通过类名调用元素可以:

  • 不需要使用内联事件监听器

  • 一次调用所有菜单项

以下是JavaScript代码:

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>

切换菜单的可见性 + 在其他地方点击时关闭

如果您希望在用户单击菜单之外的任何区域时关闭已打开的菜单,则需要在文档本身上添加事件侦听器。因此,您将不再为每个菜单按钮设置一个事件侦听器,而是将单个事件侦听器监视文档中发生的任何单击。

事件侦听器将确定单击的项是否为菜单按钮,在这种情况下,它将运行菜单处理程序。否则,它将关闭最后打开的菜单项。

JavaScript 代码:

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)

完整代码如下:

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>


太棒了Ivan。但正如31piy所提到的,实际的切换功能不再起作用。如果父级的切换功能保持不变,同时添加新功能,那将是理想的。 - Sergio
@Ivan 太完美了!非常感谢!如果在菜单项之外的文档上单击(不是菜单项或下拉框),有没有关闭任何打开的下拉菜单的想法? - Sergio
我已经在我的答案中添加了第三部分。这实际上是处理鼠标点击的更有效的方法:有一个单一的事件监听器(与第一个版本添加每个菜单项一个监听器相比)。 - Ivan
太棒了Ivan!完美运行……比我最初处理这个看似简单的任务所需的代码少得多。谢谢,我的兄弟! - Sergio

2

很难与伊万的答案竞争,但这是我的解决方案:

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>

这对我有效。不确定它是否适用于其他人。欢迎反馈。


我该如何使用这个类?您能否简要介绍一下在HTML中使用多个下拉菜单的示例? - gil.code
1
@gil.code,我稍微重构了一下我的代码,并在上面添加了一个HTML片段。希望这有所帮助。 - Tintin81

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接