addclass also to parents

123 Views Asked by At

I'm building a multi level menu and would like to add an active class to the element through a function (which is working) but would also want to add this class to it parents elements.

<ul class="main-menu">
    <li><a href="#" class="nav-link">Maine Level 1</a>
        <ul>
        <li><a class="nav-link" scene="scene_1">Scene 1</a></li>
        <li><a class="nav-link" scene="scene_2">Scene 2</a></li>
        </ul>
    </li>
    <li><a href="#" class="nav-link">Maine Level 2</a>
        <ul>
        <li><a class="nav-link" scene="scene_3">Scene 3</a></li>
            <ul>
            <li><a class="nav-link" scene="scene_3.1">Scene 3.1</a></li>
            <li><a class="nav-link" scene="scene_3.2">Scene 3.2</a></li>
            </ul>
        <li><a class="nav-link" scene="scene_4">Scene 4</a></li>
        </ul>
    </li>
</ul>

the function :

function set_active_scene(name){
    $("ul.main-menu ul li a").each(function() {
        $(this.closest('li')).removeClass("active");
        if ($(this).attr("scene") == name)
            $(this.closest('li')).addClass("active");
    });
}

I tried with $(this.parents('li')).addClass("active"); but it's not working. Any idea ?

2

There are 2 best solutions below

1
Creative Sequence On BEST ANSWER

If you only want to add the "active" class to the immediate parent li element of the active anchor element, you can simply use parent(). Here's the modified function:

function set_active_scene(name) {
    $("ul.main-menu ul li a").each(function() {
        $(this).closest('li').removeClass("active"); // Remove active class from all list items
        if ($(this).attr("scene") == name) {
            $(this).closest('li').addClass("active"); // Add active class to the current list item
            $(this).parent('li').addClass("active"); // Add active class to the immediate parent list item
        }
    });
}

With this modification, only the immediate parent of li element of the active a element will receive the "active" class, not all parent li elements up the DOM tree.

1
Peter Seliger On

Like with the OP's yesterdays question ... "How to efficiently / correctly handle the click-event at any list-item of a multiple nested list-based menu / navigation?" ... one first has to fix the broken and inconsistent markup including the invalid scene attribute for links.

The OP's originally provided function which has to set an active scene by a provided scene-name then can be refactored towards ...

  • allow an additional menu-root node being passed as first parameter which makes the function more generic in terms of how a menu-root has/needs to be queried

  • query the targeted link-node by its attribute-related scene-name.

  • remove the active class-name from any of the menu's list-item nodes, which gets achieved by querying all the menu's list-item nodes and removing said class-name from each of these nodes.

  • the main task, the OP is asking for gets achieved by programmatically building kind of a path of as active classified list-item nodes from inside-out ... which means always following/accessing the next closest and available list-item node, adding an active class-name to each of these nodes.

function setActiveScene(menuRoot, sceneName) {
  // - query the targeted link-node by its attributed scene-name.
  let targetNode = menuRoot.querySelector(`a[data-scene="${ sceneName }"]`);

  // - remove the `active` class-name from
  //   any of the menu's list-item nodes.
  menuRoot
    .querySelectorAll('li')
    .forEach(elmNode => elmNode.classList.remove('active'));


  // - programmatically build a path of `active`
  //   classified list-item nodes from inside-out.
  while (targetNode = targetNode.parentNode.closest('li')) {

    targetNode.classList.add('active');
  }
}

const menuRoot = document.querySelector('ul.main-menu');

setTimeout(setActiveScene, 600, menuRoot, 'scene_3.2');
setTimeout(setActiveScene, 2600, menuRoot,'scene_3.1');
setTimeout(setActiveScene, 4600, menuRoot, 'scene_3');

setTimeout(setActiveScene, 6600, menuRoot, 'scene_4');
setTimeout(setActiveScene, 8600, menuRoot, 'scene_2');
body { margin: 0; }
ul { list-style: none; margin: 0; padding: 0 0 0 20px; }
li { padding: 2px 0 2px 0; }
a { padding: 0 20px; background-color: #f2ffa7; }
li.active > a { background-color: #b1d000; }
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<ul class="main-menu">
  <li>
    <a href="#" class="nav-link">Maine Level 1</a>

    <ul>
      <li><a href="#" class="nav-link" data-scene="scene_1">Scene 1</a></li>
      <li><a href="#" class="nav-link" data-scene="scene_2">Scene 2</a></li>
    </ul>

  </li>
  <li>
    <a href="#" class="nav-link">Maine Level 2</a>

    <ul>
      <li>
        <a href="#" class="nav-link" data-scene="scene_3">Scene 3</a>

        <ul>
          <li><a href="#" class="nav-link" data-scene="scene_3.1">Scene 3.1</a></li>
          <li><a href="#" class="nav-link" data-scene="scene_3.2">Scene 3.2</a></li>
        </ul>
      </li>

      <li><a href="#" class="nav-link" data-scene="scene_4">Scene 4</a></li>
    </ul>
  </li>
</ul>

And in case the OP insists on a jQuery based solution (which does not offer any benefit over the standardized Selectors API), the above example code changes to ...

function setActiveScene($menuRoot, sceneName) {
  // - query the targeted link-node by its attributed scene-name.
  let targetNode = $menuRoot.find(`a[data-scene="${ sceneName }"]`)[0];

  // - remove the `active` class-name from
  //   any of the menu's list-item nodes.
  $menuRoot
    .find('li')
    .each((_, elmNode) => $(elmNode).removeClass('active'));


  // - programmatically build a path of `active`
  //   classified list-item nodes from inside-out.
  while (targetNode = targetNode.parentNode.closest('li')) {

    $(targetNode).addClass('active');
  }
}

const $menuRoot = $('ul.main-menu');

setTimeout(setActiveScene, 600, $menuRoot, 'scene_3.2');
setTimeout(setActiveScene, 2600, $menuRoot,'scene_3.1');
setTimeout(setActiveScene, 4600, $menuRoot, 'scene_3');

setTimeout(setActiveScene, 6600, $menuRoot, 'scene_4');
setTimeout(setActiveScene, 8600, $menuRoot, 'scene_1');
setTimeout(setActiveScene, 10600, $menuRoot, 'scene_2');
body { margin: 0; }
ul { list-style: none; margin: 0; padding: 0 0 0 20px; }
li { padding: 2px 0 2px 0; }
a { padding: 0 20px; background-color: #f2ffa7; }
li.active > a { background-color: #b1d000; }
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<ul class="main-menu">
  <li>
    <a href="#" class="nav-link">Maine Level 1</a>

    <ul>
      <li><a href="#" class="nav-link" data-scene="scene_1">Scene 1</a></li>
      <li><a href="#" class="nav-link" data-scene="scene_2">Scene 2</a></li>
    </ul>

  </li>
  <li>
    <a href="#" class="nav-link">Maine Level 2</a>

    <ul>
      <li>
        <a href="#" class="nav-link" data-scene="scene_3">Scene 3</a>

        <ul>
          <li><a href="#" class="nav-link" data-scene="scene_3.1">Scene 3.1</a></li>
          <li><a href="#" class="nav-link" data-scene="scene_3.2">Scene 3.2</a></li>
        </ul>
      </li>

      <li><a href="#" class="nav-link" data-scene="scene_4">Scene 4</a></li>
    </ul>
  </li>
</ul>