bubbling in a simple menu using html, css and javascript

旧巷老猫 提交于 2019-12-10 10:27:19

问题


In the code below for a simple menu, if the sub-menu is expanded/showing, then I need the sub-menu to collapse on several conditions:

  1. When the main menu or sub-menu LI is clicked
  2. When the viewport is larger than the defined minimum width
  3. When a click happens anywhere else on the screen

Number 2 works and number 1 works as long as I don't declare the addEventListener for the mainMenuID element. For some reason, when I add that event listener for any mainMenuID element, it takes precedence over anything else thus clicking on a main menu item other than Portfolio or its sub-menu items, expands and collapses the menu, but clicking on the submenuID or the ulID does nothing. As soon as I remove the eventlistener for mainMenuID, clicking on submenuID or ulID expand/collapse the sub-menu.

The second question is, how do I add a click event listener to the rest of the page so that if the sub-menu is showing, the click collapses the sub-menu.

Thank you!

<!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>

<> Hi Robidu, I had this menu working using just CSS, but I was led to use JS for some of its functionality, which stopped most of the CSS from working. So at glacier speeds, I’ve been trying to learn JS on the fly. Believe it or not, learning from videos, W3Schools, etc…. I’ve been writing, re-working, and kludging this menu into existence for the better part of 5 or 6 weeks. Totally ridiculous. It seems you understand and are readily familiar with JS. It would take me many more weeks of trying to figure out how to implement all of your advice, and along the way, I would very likely destroy what already works. Is there any way I could kindly ask you to please save me that pain by providing me with either the code snippets for where they need to go, or just more specific advice. I’ve excerpted below the parts of your text about which I have questions (because of the character limits on comments, I have to split them across multiple comments):

  1. “…you should use CSS to hide the submenu since I think that this is to be the default.” q. Yes, it is the default. I thought I was already doing this in “nav ul li.showSubmenu > ul {…}” in my CSS.
  2. “…implement an initialization stage that fires on DOMContentReady to set up a support infrastructure.” q. This, I would never know how to figure out myself.
  3. “…need a flag that indicates whether or not the menu is open so that the handler can react accordingly…spare yourself the problem with "toggle"…have the menu's state at hand…” q. I thought the “toggling” of the class “showSubmenu” was serving as that kind of ‘flag’. How and where would I use the variable in place of the toggle?
  4. “…spare yourself…repeatedly attaching and removing event listeners.”, and later on “If the menu is hidden, it doesn't get any events at all.” q. Are you saying to assign the listeners to variables anywhere in the JS code? Would I not still need to refer to those variables in the same places where I use the listeners now and in the same manner?
  5. “…attach an event listener that checks for mouse events to the the item that opens the submenu here.” q. You mean other than the listener I have on id=“submenuID”? What would be different about what you are advising?
  6. “…attach a resize event handler to the window that performs this check...” q. I cropped the screen width code from W3’s examples. How/where would using innerWidth/innerHeight be different?
  7. “…set the bubbling stage to true to intercept the event”. q. I haven’t been able to find any explanation about bubbling works if you apply one kind to the items in a menu system, and another kind to other elements on a page, etc. I understand the basics, but couldn’t figure out why it wasn’t working on this simple menu when all three listeners were used.
  8. “Collapsing the menu when a menu item is clicked…attach an event handler to each individual menu item (the best spot would be the links inside the menu”. q. Would I have to assign an ID to each LI?

  9. Overall question: I used a NAV element with ULs and LIs because I was told it’s the most accessible for screen readers. Is there a better way to do what I’m trying to do with this menu or am I sort of on the right track?

I sincerely apologize for all the follow-up questions/clarifications, but I’m sincerely spinning my wheels on this, going in circles, in rough seas, wearing a blindfold…. and any other analogy you can throw into the mix.

I would truly appreciate any specific coding you can provide to demonstrate the specifics of your advice. (or even point me to examples so that I can try and figure them out).


回答1:


A bit of retooling should do the trick here.

First of all, you should use CSS to hide the submenu since I think that this is to be the default. Furthermore you should implement an initialization stage that fires on DOMContentReady to set up a support infrastructure. This way you spare yourself the hassle with repeatedly attaching and removing event listeners. You would want to attach an event listener that checks for mouse events to the item that opens the submenu here. You would also need a flag that indicates whether or not the menu is open so that the handler can react accordingly (this way you spare yourself the problem with "toggle" and the likes, plus you immediately have the menu's state at hand if you need to do any additional checks). A simple variable should do the trick here. The plus side: If the menu is hidden, it doesn't get any events at all.

If you are interested in the window size, I'd recommend that you switch to window.innerWidth / window.innerHeight instead, because that is a numeric value that you get and which can easily be compared to the minimum size that you require. Just attach a resize event handler to the window that performs this check, and you are set. If the window size cuts below your minimum, just force the menu to collapse.

As far as collapsing the menu if the user clicks anywhere within the document, attaching an event listener looking for mouse clicks / key presses to the document object does the trick here (set the bubbling stage to true to intercept the event before anything else can happen).

Collapsing the menu when a menu item is clicked can best be achieved by attaching an event handler to each individual menu item (the best spot would be the links inside the menu - set the bubbling stage to false) that merely closes the menu.

EDIT:

I have done some tinkering now by taking your HTML and CSS, and this is what I have come up with (please note that I have also transformed the file to XHTML - but that's up to you whether or not you want to do that):

<?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>

Please see the annotations in the (X)HTML for details on what's going on there. By working on this I have discovered that I could greatly simplify the method that I have mentioned yet again so it just boils down to three events:

  • Resize: Switches the menu layout when the window width undercuts a certain threshold
  • Mouse click on menu item: Opens or closes the submenu
  • Mouse click anywhere else: Closes the menu

As for your questions...

ad 1.: I have done a bit of retooling of the CSS that you have provided. I have tossed some definitions out and have tied hiding any elements to the aria-hidden attribute (if that is set to true, the element is not displayed). Said attrtibute also helps a lot in improving accessibility. In this example, if you cannot see it on the screen, a screenreader won't display it, either.

ad 2.: It's rather simple to set up. Just include document.addEventListener('DOMContentLoaded', function (p_event) { }, false); in the main execution path and add anything that needs to be set up into the function. This is crucial especially for so-called unobtrusive JavaScript (that is, JavaScript that itself attaches hooks to the document where needed instead of hard-coding them into the (X)HTML).

ad 3.: You can get that done with a simple variable (a boolean that indicates whether the menu is open). You can then quickly check on the menu's state without having to query the DOM.

ad 4.: That is extremely tedious and costly at best so handlers are only attached once. The rest should be up to the control logic whether any events are silently ignored (by just returning if certain conditions aren't met).

ad 5.: It takes a handler of the sort that you have implemented, but I have simplified it a bit to avoid calling the DOM where unnecessary (it resorts to the aforementioned flag), plus it also takes into account the window's width for adjusting the text.

ad 6.: innerWidth / innerHeight are numeric values that can easily be returned by the DOM. I don't know about your method, but by my reckon it seems to be a bit expensive, plus when you store the value in a variable, you can perform multiple checks on it (e. g. if you need to check for different widths/heights), which just requires simple comparisons. Your approach would require you to reset the matching condition.

ad 7.: I need to correct myself here, because by chewing on your problem I found that everything (i. e. the third parameter to addEventListener) should be set to false. Otherwise the order of execution is messed up, or some links don't see the event in the first place.

ad 8.: Turned out to be unnecessary as well. I had derived my initial answer from a context menu that I have implemented in JavaScript, but because of its nature I had to resort to these handlers to close it. Here things are a little different so things can be simplified just by omitting these handlers.

ad 9.: You actually have picked the best way here. When implementing a navigation, I'm also using this approach.

I hope I could turn some of your question marks into exclamation marks. However, if you still have questions, by all means, please ask. There's nothing worse than questions left unanswered.



来源:https://stackoverflow.com/questions/52016151/bubbling-in-a-simple-menu-using-html-css-and-javascript

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!