切换元素并在单击元素外部时移除其可见性 - JavaScript

Toggle An Element And Also Remove Its Visibility When Clicking Outside Of The Element - JavaScript

我有一个包含两个子菜单项的导航。我目前有一个 class 可以打开和关闭,单击时会显示这些子菜单。

我希望当我点击页面上的任何地方时,如果它们可见,它们就会消失。

目前我认为我的代码对于目前实现的功能来说有点冗长,也许在单击时使用 e.target 会更好?

您目前可以通过单击任一菜单项(包括再次单击可见菜单项)来关闭和打开菜单。

我想通过单击菜单项外部来删除 'visible' class 我可以对整个文档做一个简单的 document.addEventListener('click', function(e) {}) 来删除 'visible' class 如果它正在显示,但这似乎不起作用。

注意:我需要在不使用 blur 事件侦听器的情况下执行此操作

代码笔:https://codepen.io/emilychews/pen/bGWVVpq

var menu_item_1 = document.getElementById('item-1'),
    menu_item_2 = document.getElementById('item-2'),
    sub_menu_item_1 = document.getElementById('sub-item-1'),
    sub_menu_item_2 = document.getElementById('sub-item-2')

if (menu_item_1) {
      menu_item_1.addEventListener('click', function(e){
        sub_menu_item_1.classList.toggle('visible')

       // hide submenu 2
        sub_menu_item_2.classList.remove('visible')
    }, false)
}

if (menu_item_2) {
      menu_item_2.addEventListener('click', function(e){
        sub_menu_item_2.classList.toggle('visible')

        // hide submenu 1
        sub_menu_item_1.classList.remove('visible')
    }, false)
}
body {
  display: flex;
  justify-content: center;
  margin: 0;
  height: 100vh;
  width: 100%;
}

header {
  margin-top: 2rem;
  display: flex;
  width: 50%;
  justify-content: space-evenly;
  align-items: center;
  padding: 1rem;
  background: red;
  height: 2rem;
}

.menu-item {
  position: relative;
  padding: 1rem;
  background: yellow;
  cursor: pointer;
}

.submenu {
  display: none; /* changes to 'block' with javascript */
  padding: 1rem;
  background: lightblue;
  position: absolute;
  top: 4rem;
  left: 0;
  width: 6rem;
}

.submenu.visible {
  display:block;
}
<header>
  <div id="item-1" class="menu-item menu-item-1">ITEM 1
    <div id="sub-item-1" class="submenu submenu-1">SUB-ITEM-1</div>
  </div>
  <div id="item-2" class="menu-item menu-item-2">ITEM 2
    <div id="sub-item-2" class="submenu submenu-2">SUB-ITEM-2</div>
  </div>
</header>

解决此问题的一种方法是利用 focusblur 事件。 div 元素默认不接收焦点,但我们可以添加 tabindex 属性来解决这个问题。

当您单击 div 时,它会获得焦点,因此我们只需监听 blur 事件并隐藏 div。

var menu_item_1 = document.getElementById('item-1'),
    menu_item_2 = document.getElementById('item-2'),
    sub_menu_item_1 = document.getElementById('sub-item-1'),
    sub_menu_item_2 = document.getElementById('sub-item-2')

if (menu_item_1) {
      menu_item_1.addEventListener('click', function(e){
        sub_menu_item_1.classList.toggle('visible')

       // hide submenu 2
        sub_menu_item_2.classList.remove('visible')
    }, false)
}

if (menu_item_2) {
      menu_item_2.addEventListener('click', function(e){
        sub_menu_item_2.classList.toggle('visible')

        // hide submenu 1
        sub_menu_item_1.classList.remove('visible')
    }, false)
}

// listen for blur events
 menu_item_1.addEventListener('blur', function(e){  sub_menu_item_1.classList.remove('visible')
})

 menu_item_2.addEventListener('blur', function(e){  sub_menu_item_2.classList.remove('visible')
})
body {
  display: flex;
  justify-content: center;
  margin: 0;
  height: 100vh;
  width: 100%;
}

header {
  margin-top: 2rem;
  display: flex;
  width: 50%;
  justify-content: space-evenly;
  align-items: center;
  padding: 1rem;
  background: red;
  height: 2rem;
}

.menu-item {
  position: relative;
  padding: 1rem;
  background: yellow;
  cursor: pointer;
}

.submenu {
  display: none; /* changes to 'block' with javascript */
  padding: 1rem;
  background: lightblue;
  position: absolute;
  top: 4rem;
  left: 0;
  width: 6rem;
}

.submenu.visible {
  display:block;
}
<header>
  <div id="item-1" class="menu-item menu-item-1" tabindex="-1">ITEM 1
    <div id="sub-item-1" class="submenu submenu-1" tabindex="-1">SUB-ITEM-1</div>
  </div>
  <div id="item-2" class="menu-item menu-item-2" tabindex="-1">ITEM 2
    <div id="sub-item-2" class="submenu submenu-2" tabindex="-1">SUB-ITEM-2</div>
  </div>
</header>

有几种不同的方法可以实现这一点,并非所有方法都涉及 JS,我将在下面概述几种可能的方法:

纯CSS:

第一个(可能也是最简单的)是仅使用 css。这再次使用 tabindex="-1" like to make your menu item buttons focusable. Once a button is focused, you can apply some CSS to the focused item's associated submenu using the :focus pseudo-class 选择器:

.menu-item:focus > .submenu { /* select the focused menu-item's child elements with the class submenu */
  display: block;
}

参见下面的示例:

body {
  display: flex;
  justify-content: center;
  margin: 0;
  height: 100vh;
  width: 100%;
}

header {
  margin-top: 2rem;
  display: flex;
  width: 50%;
  justify-content: space-evenly;
  align-items: center;
  padding: 1rem;
  background: red;
  height: 2rem;
}

.menu-item {
  position: relative;
  padding: 1rem;
  background: yellow;
  cursor: pointer;
}

.submenu {
  display: none; /* changes to 'block' with CSS */
  padding: 1rem;
  background: lightblue;
  position: absolute;
  top: 4rem;
  left: 0;
  width: 6rem;
}

.menu-item:focus > .submenu {
  display: block;
}
<header>
  <div id="item-1" class="menu-item menu-item-1" tabindex="-1">ITEM 1
    <div id="sub-item-1" class="submenu submenu-1">SUB-ITEM-1</div>
  </div>
  <div id="item-2" class="menu-item menu-item-2" tabindex="-1">ITEM 2
    <div id="sub-item-2" class="submenu submenu-2">SUB-ITEM-2</div>
  </div>
</header>

这样做的主要缺点是我们使用的是 :focus,这意味着如果您再次单击某个菜单项,它将保持聚焦而不是模糊,结果将保留菜单项目在视图中而不是隐藏它。不过,以下使用 JS 的方法可以处理这种情况:

正在向文档添加事件侦听器:

另一种可能的解决方案是更新您的 JS。这涉及使用 querySelectorAll(). You can then add event listeners to your menu-items by looing through the NodeList returned by the call to .querySelectorAll(). When you click on a menu-item, you can grab its associated submenu item using .querySelector() on the current menuItem. In order to hide the items when you click elsewhere on the screen, you can listen for click events on the document by adding an event listener to that, and hide your submenu items accordingly. Within your event listeners that you add to your menu items, you can call .stopPropagation() 选择所有菜单项和子菜单项,以防止菜单项上的单击事件冒泡到文档并导致文档事件侦听器执行(并隐藏所有项)。

const menuItems = document.querySelectorAll(".menu-item"); // Get all menu items in an array-like structure (NodeList)
const submenuItems = document.querySelectorAll(".submenu"); // select all submenu items

const hideMenus = (menus, ignore) => menus.forEach(menu => { // loop through all items (use: [...menus].forEach((menu) => {) for better browser support)
  if (menu !== ignore) // if we encounter an element that we want to keep visible, skip it, otherwise, remove its visibility
    menu.classList.remove("visible");
});

menuItems.forEach(menuItem => { // loop through the NodeList menu items
  menuItem.addEventListener("click", (e) => {
    e.stopPropagation(); // stop event from bubbling up to the document and executing the below `document.addEventListener()`  when menu item is clicked
    if (e.target === menuItem) { // don't hide when we click on a sub-menu-item (e.target = child sub-menu-item if that is clicked)
      const thisSubmenu = menuItem.querySelector(".submenu");
      thisSubmenu.classList.toggle('visible'); // toggle visibility of submenu under our item
      hideMenus(submenuItems, thisSubmenu); // hide all other submenus
    }
  });
});

document.addEventListener("click", (e) => {
  hideMenus(submenuItems);
});
body {
  display: flex;
  justify-content: center;
  margin: 0;
  height: 100vh;
  width: 100%;
}

header {
  margin-top: 2rem;
  display: flex;
  width: 50%;
  justify-content: space-evenly;
  align-items: center;
  padding: 1rem;
  background: red;
  height: 2rem;
}

.menu-item {
  position: relative;
  padding: 1rem;
  background: yellow;
  cursor: pointer;
}

.submenu {
  display: none;
  /* changes to 'block' with javascript */
  padding: 1rem;
  background: lightblue;
  position: absolute;
  top: 4rem;
  left: 0;
  width: 6rem;
}

.submenu.visible {
  display: block;
}
<header>
  <div id="item-1" class="menu-item menu-item-1">ITEM 1
    <div id="sub-item-1" class="submenu submenu-1">SUB-ITEM-1</div>
  </div>
  <div id="item-2" class="menu-item menu-item-2">ITEM 2
    <div id="sub-item-2" class="submenu submenu-2">SUB-ITEM-2</div>
  </div>
</header>

使用事件委托:

您可以更新上面的示例以使用 event delegation, which allows you to only use one event listener on the document rather than adding one per menu item (thus helping limit the resources used by your browser). You can then use e.target and .closest() 来确定您单击的元素(有关详细信息,请参阅代码注释):

const submenuItems = document.querySelectorAll(".submenu"); // select all submenu items

const hideMenus = (menus, ignore) => menus.forEach(menu => { // loop through all items (use: [...menus].forEach((menu) => {) for better browser support)
  if(menu !== ignore) // if we encounter an element that we want to keep visible, skip it, otherwise, remove its visibility
    menu.classList.remove("visible");
});

document.addEventListener("click", (e) => {
  const clickedItem = e.target, menuItem = clickedItem.closest(".menu-item");
  //       v-- use `= menuItem && menuItem.querySelector(...)` for better browser support
  const thisSubmenu = menuItem?.querySelector(".submenu"); // grab the submenu from the menuItem we clicked on (or parent menuItem if we clicked on a submenu item)
  
  if(clickedItem === menuItem) // we clicked on a menu-item
    thisSubmenu.classList.toggle('visible'); // toggle visibility of submenu under our menu-item
    
  hideMenus(submenuItems, thisSubmenu);    
});
body {
  display: flex;
  justify-content: center;
  margin: 0;
  height: 100vh;
  width: 100%;
}

header {
  margin-top: 2rem;
  display: flex;
  width: 50%;
  justify-content: space-evenly;
  align-items: center;
  padding: 1rem;
  background: red;
  height: 2rem;
}

.menu-item {
  position: relative;
  padding: 1rem;
  background: yellow;
  cursor: pointer;
}

.submenu {
  display: none; /* changes to 'block' with javascript */
  padding: 1rem;
  background: lightblue;
  position: absolute;
  top: 4rem;
  left: 0;
  width: 6rem;
}

.submenu.visible {
  display:block;
}
<header>
  <div id="item-1" class="menu-item menu-item-1">ITEM 1
    <div id="sub-item-1" class="submenu submenu-1">SUB-ITEM-1</div>
  </div>
  <div id="item-2" class="menu-item menu-item-2">ITEM 2
    <div id="sub-item-2" class="submenu submenu-2">SUB-ITEM-2</div>
  </div>
</header>

我还在每个子菜单上添加了 console.log 以确保它们在菜单关闭前是交互式的。

const menus = Array.from(document.querySelectorAll('.menu-item'));

function handleOnClickOutsideMenu(e) {
  const target = menus.filter(menu => menu.contains(e.target));
  if (target.length) {
     // user is clicking inside a menu: don't do anything.
     // this is handled by handleOnMenuToggle.
     return;
  }
  // close all the menus in the page
  menus.forEach(menu => menu.classList.remove('expanded'));
  // we don't need it anymore (it is added dynamically in the handleOnMenuToggle)
  document.removeEventListener('click', handleOnClickOutsideMenu);  
}

function handleOnMenuToggle(e) {
  // close other menus
  menus
    .filter(menu => menu !== e.currentTarget)
    .forEach(menu => menu.classList.remove('expanded'));
  // toggle current menu
  e.currentTarget.classList.toggle('expanded');

  // Important optimization:
  // we want the click event on the document only when a menu is expanded
  if (e.currentTarget.classList.contains('expanded')) {       
    document.addEventListener('click', handleOnClickOutsideMenu);
  } else {
    document.removeEventListener('click', handleOnClickOutsideMenu);     
  }
}

menus.forEach(menu => {
  menu.addEventListener('click', handleOnMenuToggle); 
});
body {
  display: flex;
  justify-content: center;
  margin: 0;
  height: 100vh;
  width: 100%;
}

header {
  margin-top: 2rem;
  display: flex;
  width: 50%;
  justify-content: space-evenly;
  align-items: center;
  padding: 1rem;
  background: red;
  height: 2rem;
}

.menu-item {
  position: relative;
  padding: 1rem;
  background: yellow;
  cursor: pointer;
}

.submenu {
  display: none;
  padding: 1rem;
  background: lightblue;
  position: absolute;
  top: 4rem;
  left: 0;
  width: 6rem;
}

/* adding .expanded on menu-item so it can handle multiple sub menu */
.menu-item.expanded .submenu {
  display: block;     
}
<header>
  <div id="item-1" class="menu-item menu-item-1">ITEM 1
    <div id="sub-item-1" class="submenu submenu-1" onclick="console.log(this)">SUB-ITEM-1</div>
  </div>
  <div id="item-2" class="menu-item menu-item-2">ITEM 2
    <div id="sub-item-2" class="submenu submenu-2" onclick="console.log(this)">SUB-ITEM-2</div>
  </div>
</header>