简体   繁体   中英

How do I add and remove classes to multiple child elements nested in an UL with a single click using Javascript?

I want to start by apologizing for such a long question, I just hope I wont make it difficult to understand as a result.

I have created a side bar with three Menu elements in an UL which expand to show child elements, change background color and remove hover effect when clicked. I did this by defining a function that adds and remove classes containing relevant properties when the menu element is clicked.

The four specific things that I want the sidebar to do but cant seem to get it to do are as follows;

  1. only one selected/clicked item to expand at a time while all the rest of the unselected menu elements are collapsed. That means if I click the first item, it expands and when I click the second one, the first one collapses while the one I clicked expands etc.
  2. The selected/clicked element changes its background color to indicate it's selected.
  3. The selected/clicked element has no hover effect on the text while the unselected elements have a hover effect of text color change on them.
  4. I also want the selected menu element be able to toggle the expansion on and off not affecting the other elements in the UL.

I think where I'm having most trouble with my code is in the adding and removal of classes especially given that the <a> Tags which are nested inside the <li> Tags (clicked elements) are where the "hover" class needs to be added/removed, as well as the <ul> Tags that expand are also nested inside the clicked elements.

 function toggleMenu(e) { var kids = document.querySelector("#menuList").children; var unselectedLink = document.querySelectorAll(".unselected a"); var unselectedDropdown = document.querySelectorAll(".unselected ul"); //adds "unselected" class to all elements exept the selected one for (var i = 0; i < kids.length; i++) { kids[i].className = "unselected"; } //adds "menuHover" class to all elements exept the selected element for (var i = 0; i < unselectedLink.length; i++) { unselectedLink[i].className = "menuHover"; } for (var i = 0; i < unselectedDropdown.length; i++) { unselectedDropdown[i].classList.remove("show") } //adds "selected" class, removes "menuHover" class and adds "toggle" to the selected element e.className = "selected"; document.querySelector(".selected a").classList.remove("menuHover"); document.querySelector(".selected ul").classList.toggle("show"); }
 .sidebar { position: fixed; width: 250px; height: 100%; left: 0px; top: 0; background: #1b1b1b; font-family: sans-serif; }.menu-bar { background: #1b1b1b; height: 60px; display: flex; align-items: center; padding-left: 42px; }.side-text { color: #C5C5C5; font-weight: bold; font-size: 20px; } nav ul { background: #1b1b1b; height: 100%; width: 100%; list-style: none; margin-left: 0; padding-left: 0; } nav ul li { line-height: 40px; } nav ul li a { position: relative; color: #C5C5C5; text-decoration: none; font-size: 14px; padding-left: 43px; font-weight: normal; display: block; width: 100%; } nav ul ul { position: static; display: none; } nav ul ul li a { font-family: sans-serif; font-size: 13px; color: #e6e6e6; padding-left: 80px; font-weight: lighter; }.submenu-item:hover { background: #1e1e1e;important. } /*...........selected and show..................*/:selected { background-color; #255DAA. }:show { display; block. } /*...........unselected and hover..................*/:unselected { color; #1e1e1e. }:menuHover:hover { color; #255DAA; }
 <nav class="sidebar"> <div class="menu-bar"> <label class="side-text">MENU</label> </div> <ul id="menuList"> <li class="selected" onclick="toggleMenu(this)"> <a href="#" class="" id="staff-btn">Staff</a> <ul> <li><a href="#">New Staff</a></li> <li><a href="#">View Staff</a></li> </ul> </li> <li class="unselected" onclick="toggleMenu(this)"> <a href="#" id="notes-btn" class="menuHover">Notes</a> <ul> <li><a href="#">New Note</a></li> <li><a href="#">Edit Notes</a></li> </ul> </li> <li class="unselected" onclick="toggleMenu(this)"> <a href="#" class="menuHover" id="tasks-btn">Tasks</a> <ul> <li><a href="#">New Tasks</a></li> <li><a href="#">Edit Task</a></li> </ul> </li> </nav>

I am quite close but somehow, logically I am doing things the wrong way with the JavaScript, so any adjustments to the code to make it reach all four of the above goals will be much appreciated. thanks

A simple way to do this:

If the element was selected, just unselect it

If it wasnt, unselect all elements and select the clicked element

function toggleMenu(el) {
  if (el.classList.contains("selected")) {
    el.classList.remove("selected");
    el.classList.add("unselected");
  }
  else {
    for (const child of document.getElementById("menuList").children) {
      child.classList.remove("selected");
      child.classList.add("unselected");
    }
    el.classList.remove("unselected");
    el.classList.add("selected");
  }
}

Edit 1 You can use the following css to unhide the submenu of a selected menuitem:

.selected ul {
  display: block;
}

Edit 2 I went to the trouble of actually implementing it.

 function toggleMenu(el) { if (el.classList.contains("selected")) { el.classList.remove("selected"); el.classList.add("unselected"); } else { for (const child of document.getElementById("menuList").children) { child.classList.remove("selected"); child.classList.add("unselected"); } el.classList.remove("unselected"); el.classList.add("selected"); } }
 .sidebar { position: fixed; width: 250px; height: 100%; left: 0px; top: 0; background: #1b1b1b; font-family: sans-serif; }.menu-bar { background: #1b1b1b; height: 60px; display: flex; align-items: center; padding-left: 42px; }.side-text { color: #C5C5C5; font-weight: bold; font-size: 20px; } /* menu */.menu { background: #1b1b1b; height: 100%; width: 100%; list-style: none; margin-left: 0; padding-left: 0; }.menu-item { line-height: 40px; }.menu-item a { position: relative; color: #C5C5C5; text-decoration: none; font-size: 14px; padding-left: 43px; font-weight: normal; display: block; width: 100%; } /* submenu */.submenu { position: static; display: none; list-style: none; }.submenu-item a { font-family: sans-serif; font-size: 13px; color: #e6e6e6; padding-left: 80px; font-weight: lighter; }.submenu-item:hover { background: #1e1e1e; } /* selected and unselected */.selected { background-color: #255DAA; }.selected.submenu { display: block; }.unselected { color: #1e1e1e; }.unselected:hover a { color: #255DAA; }
 <nav class="sidebar"> <div class="menu-bar"> <label class="side-text">MENU</label> </div> <ul class="menu" id="menuList"> <li class="menu-item selected" onclick="toggleMenu(this)"> <a href="#" id="staff-btn">Staff</a> <ul class="submenu"> <li class="submenu-item"><a href="#">New Staff</a></li> <li class="submenu-item"><a href="#">View Staff</a></li> </ul> </li> <li class="menu-item unselected" onclick="toggleMenu(this)"> <a href="#" id="notes-btn">Notes</a> <ul class="submenu"> <li class="submenu-item"><a href="#">New Note</a></li> <li class="submenu-item"><a href="#">Edit Notes</a></li> </ul> </li> <li class="menu-item unselected" onclick="toggleMenu(this)"> <a href="#" id="tasks-btn">Tasks</a> <ul class="submenu"> <li class="submenu-item"><a href="#">New Tasks</a></li> <li class="submenu-item"><a href="#">Edit Task</a></li> </ul> </li> </ul> </nav>

As alternative to the accepted answer, here two different approaches I was working on...

#menuList li is a container for a list of <a> menu items which have adjacent <ul> sub-menu-items. For easy selection with CSS I assigned class .menu-item to those <a> .

The CSS logic for both versions is essentially equal:

  • set the .menu-item adjacent ul sub-menu-items hidden by default
  • define :hover colors
  • define colors for a selected .menu-item and adjacent ul (either :focus or .selected is true)
  • make .menu-item adjacent ul sub-menu-items visible when a .menu-item gets selected (ditto)

Difference: for CSS only we use the :focus selector, for CSS with Javascript we use class .selected .

CSS only (automatic focus and blur )

An <a> gets focus when clicked (like button, input, etc. :focus is true ). When the user clicks/taps outside the focussed element it automatically loses focus again (gets blurred and :focus is false , as in :not(:focus) = 'blur' ). We can use the CSS :focus selector to handle user clicks and modify elements, MDN: ':focus' .

CSS with Javascript ( focus and blur on request)

The OP wants a selected .menu-item and its adjacent ul sub-menu-items to stay visible until the user specifically deselects it again. This cannot be done with the :focus selector, so we ignore that selector and use class .selected instead to handle focus and blur requirements ourselves, MDN: HTMLElement.blur() .

The Javascript logic is fairly straightforward:

Attach a 'click'- eventListener ( MDN: Element: click event ) to main container #menuList handling:

  • when a .menu-item gets selected and it is the currently .selected then blur it ( menuItemBlur() )
  • otherwise
    • when we have a previously selected .menu-item open, blur that first ( menuItemBlur() )
    • and then focus the newly selected .menu-item ( menuItemFocus() )

Changes to OP code

  • removed unneeded CSS
  • removed unneeded class attributes from HTML
  • changed href="#" in <#menuList li a> to href="javascript:void(0)" to prevent it from creating an entry in the browser history (sub-menu-items will still create an entry).

The below snippet is heavily commented and should be self-explanatory.

 'use-strict'; var activeItem; // Holds the currently '.selected''.submenu' (null/undefined if none) // Attach 'click' event listener to the #menuList document.getElementById('menuList').addEventListener('click', function(e) { menuItemToggle(e.target) }); function menuItemToggle(el) { if (el.classList.contains('menu-item2')) { // When a '.menu-item' gets clicked (not its kids) if (el.classList.contains('selected')) { // and it is the '.selected''.menu-item' menuItemBlur(el); // then close it and remove focus() } else { if (activeItem) // When there is a currently selected '.menu-item' menuItemBlur(activeItem); // then deactivate it menuItemFocus(el); // Now activate the clicked `.menu-item` }; }; function menuItemBlur(el) { el.classList.remove("selected"); // Set the '.menu-item' to not '.selected' activeItem = null; // and remove the reference to it el.blur(); // Remove focus from element for CSS ':focus' //...extend with other 'Blur' stuff... }; function menuItemFocus(el) { el.classList.add("selected"); // Set the '.menu-item' to '.selected' activeItem = el; // and save a reference to it //...extend with other 'Focus' stuff... }; };
 .sidebar { position: fixed; width: 250px; height: 100%; left: 0px; top: 0; background: #1b1b1b; font-family: sans-serif; }.menu-bar { background: #1b1b1b; height: 60px; display: flex; align-items: center; padding-left: 42px; }.side-text { color: #c5c5c5; font-weight: bold; font-size: 20px; } nav ul { background: #1b1b1b; height: 100%; width: 100%; list-style: none; margin-left: 0; padding-left: 0; } nav ul li { line-height: 40px; } nav ul li a { position: relative; color: #c5c5c5; text-decoration: none; font-size: 14px; padding-left: 43px; font-weight: normal; display: block; width: 100%; } nav ul ul { position: static; display: none; } nav ul ul li a { font-family: sans-serif; font-size: 13px; color: #e6e6e6; padding-left: 80px; font-weight: lighter; } /*************/ /* ADDED CSS */ /*************/ /* All classes starting with "menu-item" */ [class^="menu-item"] + ul { display: none } /* hide adjacent UL */ [class^="menu-item"]:hover { color: #255daa } /* hover color */ a + ul li a:hover { color: #c5c5c5; background-color: #1b1b1b } /* menu-item adjacent sub-menu-items hover colors Here the generic form is used, but it would probably be more clear to be specific and use: - either.menu-item1:focus + ul li a:hover - or.menu-item2.selected + ul li a:hover */ /* ':focus' version This version uses the CSS ':focus' without any Javascript. Main difference with the '.selected' version below is that when the user clicks outside the '.menu-item', the '.menu-item' looses focus and therefore gets hidden again (as:focus is no longer true). */.menu-item1:focus, .menu-item1:focus + ul { color: #e6e6e6; background-color: #255DAA } /* focus colors */.menu-item1:focus + ul { display: block } /* show adjacent UL */ /* '.selected' version, with Javascript. Basically the same CSS, but now using class '.selected' instead of ':focus'. Closing occurs only on user specific 'click'. */.menu-item2.selected, .menu-item2.selected + ul { color: #e6e6e6; background-color: #255DAA } /* focus colors */.menu-item2.selected + ul { display: block } /* show adjacent UL */ /*********************/ /* for demo use only */ /*********************/ nav h3 { color: rgba(100, 149, 237,.9); /* CornflowerBlue */ font-style: italic; padding-left: 43px; }.anchor { color: white; padding-left: 43px; }.content { font-size: 1.5rem; margin: 5rem 300px; } /* preferred globals */ html,body { box-sizing: border-box; width: 100%; max-width: 100% } *::before,*::after, * { box-sizing: inherit } body { margin: 0 }
 <nav class="sidebar"> <div class="menu-bar"> <label class="side-text">MENU</label> </div> <h3>test</h3> <a class="anchor" href="javascript:void(0)">some '.sidebar' &lt;a&gt;</a> <ul id="menuList"> <h3>:focus version</h3> <li> <a class="menu-item1" href="javascript:void(0)">Staff</a> <ul> <li><a href="#">New Staff</a></li> <li><a href="#">View Staff</a></li> </ul> </li> <li> <a class="menu-item1" href="javascript:void(0)">Notes</a> <ul> <li><a href="#">New Note</a></li> <li><a href="#">Edit Notes</a></li> </ul> </li> <li> <a class="menu-item1" href="javascript:void(0)">Tasks</a> <ul> <li><a href="#">New Tasks</a></li> <li><a href="#">Edit Task</a></li> </ul> </li> <h3>.selected version</h3> <li> <a class="menu-item2" href="javascript:void(0)">Staff</a> <ul> <li><a href="#">New Staff</a></li> <li><a href="#">View Staff</a></li> </ul> </li> <li> <a class="menu-item2" href="javascript:void(0)">Notes</a> <ul> <li><a href="#">New Note</a></li> <li><a href="#">Edit Notes</a></li> </ul> </li> <li> <a class="menu-item2" href="javascript:void(0)">Tasks</a> <ul> <li><a href="#">New Tasks</a></li> <li><a href="#">Edit Task</a></li> </ul> </li> </ul> </nav> <div class="content"> <h3><b>Note</b></h3> <p> This demo uses two different approaches interchangeably creating a quirky behaviour, which under normal circumstances would not exist. </p> <p>To reproduce:</p> <ul> <li>select a <i>':focus version'</i> menu item first <li>then select a <i>'.selected version'</i> menu item </ul> <p> As you can see, the selected <i>':focus version'</i> loses focus and a second 'click' is needed to activate the <i>'.selected version'</i> menu item. This is because the first click event of the <i>'.selected version'</i> gets consumed by the blur event of the <i>':focus version'</i>. </p> <p>Just so you know...</p> </div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM