首页 > 解决方案 > 使用 html、css 和 javascript 在简单菜单中冒泡

问题描述

在下面的简单菜单代码中,如果子菜单展开/显示,那么我需要子菜单在几个条件下折叠:

  1. 单击主菜单或子菜单 LI 时
  2. 当视口大于定义的最小宽度时
  3. 当点击发生在屏幕上的其他任何地方时

只要我没有为 mainMenuID 元素声明 addEventListener,第 2 号就可以工作,1 号也可以。出于某种原因,当我为任何 mainMenuID 元素添加该事件侦听器时,它优先于其他任何内容,因此单击 Portfolio 或其子菜单项以外的主菜单项,展开和折叠菜单,但单击 submenuID 或ulID 什么都不做。一旦我删除了 mainMenuID 的事件监听器,单击 submenuID 或 ulID 展开/折叠子菜单。

第二个问题是,如何向页面的其余部分添加点击事件侦听器,以便在显示子菜单时单击折叠子菜单。

谢谢!

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>

<style media="screen">
body{
  background-color: black;
}

nav ul {
    position: relative;
    text-align: left;
}

nav ul li {
    display: block;  /* WHAT????  REMOVING THIS ACTUALLY MAKES TEXT-DECORATIONS REAPPEAR.... WHY???*/
}

nav a {
    text-decoration: none;
}

/* Hide dropdowns by default */
nav ul ul {
    position: relative;
    display: none;
    left: 0;
}

/* Display Dropdowns on Click */
    nav ul li.showSubmenu > ul {
    display: block;
    position: relative;
    width: 30%;
}

.menu a {
    text-decoration-style: none;
    text-decoration: none;
    background-color: black;
    color: white;
}

.menu a:hover{
    background-color: white;
    color: black;
}
</style>

<body>
    <nav id="mainMenuID" class="menu">
        <ul>
            <li class="menuItem"><a href="#">Home</a></li>
            <li class="menuItem"><a href="#">About</a></li>
            <li id="submenuItem" class="submenu_class"><a href="#"><span id="submenuID">Portfolio ▼&lt;/span></a>
            <ul id="ulID">
                <li><a href="#">Landscape</a></li>
                <li><a href="#">Architecture</a></li>
                <li><a href="#">Animal</a></li>
                <li><a href="#">Other</a></li>
            </ul>
            </li>
            <li class="menuItem"><a href="#">Information</a></li>
            <li class="menuItem"><a href="#">Contact</a></li>
       </ul>
    </nav>


<script>
function openSubmenu() {
    if (document.getElementsByClassName('showSubmenu').length == 0){
      document.getElementById("submenuItem").classList.toggle("showSubmenu");
      document.getElementById("submenuID").textContent = "Portfolio ▲";
    } else {
      document.getElementById("submenuItem").classList.toggle("showSubmenu");
      document.getElementById("submenuID").textContent = "Portfolio ▼";
    }
}


function resetSubmenu() {
    var submenuElements = document.getElementsByClassName('submenu_class showSubmenu');
    for (var i = 0; i < submenuElements.length; i++) {
    submenuElements[i].setAttribute('class', 'submenu_class');
    document.getElementById("submenuID").textContent = "Portfolio ►";
    }
}

function screenWidthFunction(x) {
    if (x.matches) {//if it's a narrow screen
        document.getElementById("submenuID").textContent = "Portfolio ▼";
        document.getElementById("ulID").addEventListener("click", openSubmenu, true);
        document.getElementById("submenuID").addEventListener("click", openSubmenu, true);
        document.getElementById("mainMenuID").addEventListener("click", openSubmenu, true);
    } else {
        resetSubmenu();
        document.getElementById("submenuID").textContent = "Portfolio ►";
        document.getElementById("ulID").removeEventListener("click", openSubmenu);
        document.getElementById("submenuID").removeEventListener("click", openSubmenu);
        document.getElementById("mainMenuID").removeEventListener("click", openSubmenu);
      }
   }

    var x = window.matchMedia("(max-width: 480px)");
    screenWidthFunction(x);
    x.addListener(screenWidthFunction);
</script>

<> 嗨 Robidu,我让这个菜单只使用 CSS 工作,但我被引导使用 JS 来实现它的一些功能,这阻止了大多数 CSS 工作。所以以冰川的速度,我一直在努力学习 JS。信不信由你,从视频、W3Schools 等中学习……。在 5 或 6 周的大部分时间里,我一直在编写、重新设计和组合这个菜单。完全荒谬。看来您了解并且很容易熟悉 JS。我将花费数周的时间来弄清楚如何实施您的所有建议,并且在此过程中,我很可能会破坏已经有效的方法。有什么方法可以请您通过向我提供他们需要去的地方的代码片段或更具体的建议来减轻我的痛苦。

  1. “……你应该使用 CSS 来隐藏子菜单,因为我认为这是默认设置。” 问。是的,这是默认设置。我以为我已经在我的 CSS 中的“nav ul li.showSubmenu > ul {…}”中这样做了。
  2. “……实现一个在 DOMContentReady 上触发的初始化阶段,以建立一个支持基础设施。” 问。这一点,我永远不知道如何弄清楚自己。
  3. “……需要一个标志来指示菜单是否打开,以便处理程序可以做出相应的反应……不用“切换”来解决问题……手头有菜单的状态……” 我认为“showSubmenu”类的“切换”是一种“标志”。我将如何以及在哪里使用变量来代替切换?
  4. “……饶了你自己……重复地附加和移除事件监听器。”,然后是“如果菜单被隐藏,它根本不会收到任何事件。” 问。您是说将侦听器分配给 JS 代码中任何位置的变量吗?我是否还需要在我现在使用侦听器的相同位置以相同的方式引用这些变量?
  5. “……将检查鼠标事件的事件侦听器附加到此处打开子菜单的项目。” 问。你的意思是除了我在 id="submenuID" 上的听众吗?你的建议有什么不同?
  6. “……将调整大小事件处理程序附加到执行此检查的窗口……” q。我从 W3 的示例中裁剪了屏幕宽度代码。使用 innerWidth/innerHeight 如何/在哪里会有所不同?
  7. “……将冒泡阶段设置为 true 以拦截事件”。问。如果您将一种应用于菜单系统中的项目,将另一种应用于页面上的其他元素等,我无法找到关于冒泡作品的任何解释。我了解基础知识,但不明白为什么当使用所有三个侦听器时,它在这个简单的菜单上不起作用。
  8. “单击菜单项时折叠菜单...为每个单独的菜单项附加一个事件处理程序(最好的位置是菜单内的链接”。问。我是否必须为每个 LI 分配一个 ID?

  9. 总体问题:我使用了带有 UL 和 LI 的 NAV 元素,因为有人告诉我它是屏幕阅读器最容易访问的元素。有没有更好的方法来做我想要用这个菜单做的事情,或者我在正确的轨道上?

我真诚地为所有后续问题/澄清道歉,但我真诚地在这方面旋转我的轮子,绕着圈子,在波涛汹涌的大海中,戴着眼罩……。以及您可以加入的任何其他类比。

我真的很感激您可以提供任何具体的编码来展示您的建议的细节。(或者甚至指向我的例子,以便我可以尝试找出它们)。

标签: javascripthtmlcssaddeventlistenerevent-bubbling

解决方案


在这里进行一些重组应该可以解决问题。

首先,您应该使用 CSS 隐藏子菜单,因为我认为这是默认设置。此外,您应该实现一个在DOMContentReady上触发的初始化阶段建立支持基础设施。这样,您就省去了重复附加和删除事件侦听器的麻烦。您可能希望将检查鼠标事件的事件侦听器附加到在此处打开子菜单的项目。您还需要一个指示菜单是否打开的标志,以便处理程序可以做出相应的反应(这样您就可以避免“切换”等问题,如果您需要,您可以立即获得菜单的状态做任何额外的检查)。一个简单的变量应该可以在这里解决问题。优点:如果菜单被隐藏,它根本不会收到任何事件。

如果您对窗口大小感兴趣,我建议您切换到window.innerWidth/ window.innerHeight,因为这是您获得的数值,并且可以很容易地与您需要的最小大小进行比较。只需将 resize 事件处理程序附加到执行此检查的窗口,就可以了。如果窗口大小低于您的最小值,只需强制菜单折叠即可。

至于在用户单击文档中的任何位置时折叠菜单,将寻找鼠标单击/按键按下的事件侦听器附加到文档对象就可以解决问题(将冒泡阶段设置为true以在其他任何事情发生之前拦截事件)。

单击菜单项时折叠菜单的最佳方法是通过将事件处理程序附加到每个单独的菜单项(最好的位置是菜单内的链接 - 将冒泡阶段设置为false)仅关闭菜单。

编辑:

我现在通过获取您的 HTML 和 CSS 进行了一些修改,这就是我想出的(请注意,我也将文件转换为 XHTML - 但是否要这样做取决于您):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xml:lang="en">
<head>
<title>Submenu Test Case</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<style type="text/css">
body {
  background-color: black;
  color: white;
  }

aside {
  width: 15em;
  float: left;
  }

nav ul {
  position: relative;
  text-align: left;
  list-style: none;         /* Kills any list decoration */
  }

nav ul ul {
  position: relative;
  left: 0;
  }

nav a {
  text-decoration: none;
  }

/* This ARIA attribute can greatly improve accessibility and can also be used
   to actually hide elements just by tying "display: none;" to it... */
[aria-hidden="true"] {
  display: none;
  }

.menu a {
  color: white;
  }

.menu a:hover, .menu span:hover {
  background-color: white;
  color: black;
  }

/* Style definitions that override settings for mobile devices go here! */
@media screen and (min-width: 480px)
  {
/* Reposition the submenu if it's on a sufficiently wide window... */
  nav ul li > ul {
    position: absolute;
    margin-top: -1.2em;
    left: 7em;
    }
  }
</style>
<script type="application/javascript">
/* <![CDATA[ */
var submenu_open = false;   // Has the submenu been opened?
var need_mobile = false;    // Are we on a narrow window?

// Takes care of hiding and showing the submenu
function ToggleMenu(p_event)
  {
// Do not activate e. g. on a right click...
  if(p_event.button != 0)
    return;

  if(submenu_open)
    {
// If the submenu has previously been open, close it (and adjust the
// controlling menu item if necessary)
    document.getElementById('sub1').setAttribute('aria-hidden', 'true');
    if(window.innerWidth < 480)
      document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ▼';
    }
  else
    {
// If the submenu has previously been closed, open it (and adjust the
// controlling menu item if necessary)
    document.getElementById('sub1').setAttribute('aria-hidden', 'false');
    if(window.innerWidth < 480)
      document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ▲';
    }

// This prevents the document's root node (i. e. the document object) from
// seeing the event when clicking on the superordinate item for the submenu...
  p_event.stopPropagation();

  submenu_open = !submenu_open;
  }

// Triggered upon clicking anywhere inside of the document...
function CloseMenu(p_event)
  {
  if(!submenu_open)
    return;

  document.getElementById('sub1').setAttribute('aria-hidden', 'true');
  if(window.innerWidth < 480)
    document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ▼';
  submenu_open = false;
  }

function CheckWindowSize(p_event)
  {
  if(window.innerWidth < 480)
    {
    if(need_mobile)
      return;

// On a mobile device, insert the submenu into the main one...
    if(submenu_open)
      document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ▲';
    else
      document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ▼';
    }
  else
    {
    if(!need_mobile)
      return;

// If the window is wide enough, we can display the submenu next to the main
// one...
    document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ►';
    }

  need_mobile = !need_mobile;
  }

// Initialization sequence (adds a few event handlers)
document.addEventListener('DOMContentLoaded', function (p_event) {
  document.getElementById('sub-item1').addEventListener('click', ToggleMenu, false);
  window.addEventListener('resize', CheckWindowSize, false);
  document.addEventListener('click', CloseMenu, false);

// If we are on a mobile device, adjust the text of the menu item.  
  if(window.innerWidth < 480)
    {
    need_mobile = true;
    document.getElementById('sub-item1').childNodes[0].childNodes[0].data = 'Portfolio ▼';
    }
  }, false);
/* ]]> */
</script>
</head>
<body>
<header><h1>Submenu Test Case</h1></header>
<aside>
<nav class="menu">
  <ul>
    <li class="menuItem"><a href="javascript:alert('\'Home\' triggered!');">Home</a></li>
    <li class="menuItem"><a href="javascript:alert('\'About\' triggered!');">About</a></li>
    <!-- Assume normal operation here (window width >= 480 pixels) so the
         text is set accordingly...
         Please note that I have removed some extraneous elements and
         attributes. -->
    <li class="submenu" id="sub-item1"><span>Portfolio ►&lt;/span>
    <ul id="sub1" aria-hidden="true">
      <li><a href="javascript:alert('\'Landscape\' triggered!');">Landscape</a></li>
      <li><a href="javascript:alert('\'Architecture\' triggered!');">Architecture</a></li>
      <li><a href="javascript:alert('\'Animal\' triggered!');">Animal</a></li>
      <li><a href="javascript:alert('\'Other\' triggered!');">Other</a></li>
    </ul>
    </li>
    <li class="menuItem"><a href="javascript:alert('\'Information\' triggered!');">Information</a></li>
    <li class="menuItem"><a href="javascript:alert('\'Contact\' triggered!');">Contact</a></li>
  </ul>
</nav>
</aside>
<main />
</body>
</html>

请参阅 (X)HTML 中的注释以了解其中发生的情况的详细信息。通过研究这个,我发现我可以大大简化我再次提到的方法,所以它可以归结为三个事件:

  • 调整大小:当窗口宽度低于某个阈值时切换菜单布局
  • 鼠标点击菜单项:打开或关闭子菜单
  • 鼠标点击其他任何地方:关闭菜单

至于你的问题...

广告 1.:我对您提供的 CSS 进行了一些重组。我抛弃了一些定义,并将隐藏任何元素与aria-hidden属性绑定(如果设置为 true,则不显示元素)。所述属性还有助于提高可访问性。在此示例中,如果您在屏幕上看不到它,屏幕阅读器也不会显示它。

广告 2.:设置起来相当简单。只需包含document.addEventListener('DOMContentLoaded', function (p_event) { }, false); 在主执行路径中,并将需要设置的任何内容添加到函数中。这对于所谓的非侵入式 JavaScript(即 JavaScript 本身在需要时将钩子附加到文档而不是将它们硬编码到 (X)HTML 中)至关重要。

ad 3.:你可以用一个简单的变量(一个布尔值,指示菜单是否打开)来完成。然后,您可以快速检查菜单的状态,而无需查询 DOM。

ad 4.:这非常繁琐且充其量是昂贵的,因此处理程序只连接一次。其余的应该取决于控制逻辑是否静默忽略任何事件(如果不满足某些条件则仅返回)。

ad 5.:它需要您实现的那种处理程序,但我对其进行了一些简化,以避免在不必要的地方调用 DOM(它采用上述标志),此外它还考虑了调整窗口的宽度文本。

ad 6.: innerWidth/innerHeight是 DOM 可以轻松返回的数值。我不知道您的方法,但我认为它似乎有点贵,而且当您将值存储在变量中时,您可以对其执行多次检查(例如,如果您需要检查不同的宽度/高度),只需要简单的比较。您的方法将要求您重置匹配条件。

ad 7.:我需要在这里更正自己,因为通过咀嚼您的问题,我发现所有内容(即 的第三个参数addEventListener)都应该设置为false。否则执行顺序会混乱,或者某些链接首先看不到事件。

广告 8.:原来也没有必要。我从我在 JavaScript 中实现的上下文菜单中得出了我的初步答案,但由于它的性质,我不得不求助于这些处理程序来关闭它。这里有些不同,因此可以通过省略这些处理程序来简化事情。

广告 9.:您实际上在这里选择了最好的方式。在实现导航时,我也在使用这种方法。

我希望我能把你的一些问号变成感叹号。但是,如果您仍有疑问,请务必提出。没有什么比没有回答的问题更糟糕的了。


推荐阅读