使用 html、css 和 javascript 在简单菜单中冒泡

bubbling in a simple menu using html, css and javascript

在下面的简单菜单代码中,如果子菜单是expanded/showing,那么我需要子菜单在几种情况下折叠:

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

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

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

谢谢!

<!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 ▼</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。信不信由你,从视频、W3School 等学习……。在 5 或 6 周的大部分时间里,我一直在编写、修改和拼凑这个菜单。完全荒谬。 看来您了解并且很容易熟悉 JS。我将花费更多的时间来尝试弄清楚如何实施您的所有建议,并且在此过程中,我很可能会破坏已经有效的方法。有什么办法可以请您通过向我提供他们需要去的地方的代码片段或更具体的建议来减轻我的痛苦。 我在下面摘录了您文本中我有疑问的部分(由于评论的字符数限制,我不得不将它们分成多个评论):

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

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

我真诚地为所有的后续行动道歉 questions/clarifications,但我真诚地在这上面旋转我的轮子,兜圈子,在波涛汹涌的大海中,戴着眼罩......。以及您可以加入的任何其他类比。

如果您能提供任何具体的代码来证明您的建议的细节,我将不胜感激。 (或者甚至给我指出示例,以便我可以尝试找出它们)。

稍微调整一下应该可以解决问题。

首先,您应该使用 CSS 隐藏子菜单,因为我认为这是默认设置。此外,您应该实施一个在 DOMContentReady 上触发的初始化阶段,以设置支持基础结构。这样您就可以避免重复附加和删除事件侦听器的麻烦。您可能希望将检查鼠标事件的事件侦听器附加到在此处打开子菜单的项目。您还需要一个标志来指示菜单是否打开,以便处理程序可以做出相应的反应(这样您就可以避免 "toggle" 之类的问题,而且如果你需要做任何额外的检查)。一个简单的变量应该可以解决这个问题。 好的一面:如果菜单被隐藏,它根本不会得到任何事件。

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

至于如果用户在文档中的任意位置单击则折叠菜单,将寻找鼠标单击/按键的事件侦听器附加到文档对象就可以解决这个问题(将冒泡阶段设置为 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 ►</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 中的注释,了解那里发生的事情的详细信息。 通过研究这个,我发现我可以大大简化我再次提到的方法,所以它可以归结为三个事件:

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

关于你的问题...

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

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

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

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

ad 5.: 它需要一个你已经实现的那种处理程序,但我稍微简化了它以避免在不必要的地方调用 DOM (它求助于上述标志),再加上它还考虑了 window 的宽度来调整文本。

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

ad 7.: 我需要在这里纠正自己,因为通过仔细研究你的问题我发现所有的东西(即 addEventListener 的第三个参数)都应该设置为 false。不然执行顺序乱了,或者有的链接一开始就看不到事件

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

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

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