Search code examples
javascriptqueryselector

Adjacent sibling selector element.querySelector("+element") is invalid


This simple code will work:

<!DOCTYPE html>
<html>
<body>
    <br>
    <div>
        <a href="#a1">Link 1</a>
        <a href="#a2">Link 2</a>
        <a href="#a3">Link 3</a>
        <a href="#a4">Link 4</a>
    </div>
    <a href="#c1">Link 1 <span>color me plz!</span></a>
    <br><br>
    <div>
        <a href="#b1">Link 1</a>
        <a href="#b2">Link 2</a>
        <a href="#b3">Link 3</a>
        <a href="#b4">Link 4</a>
    </div>
    <a href="#c1">Link 1 <span>color me plz!</span></a>
    <a href="#c2">Link 2 <span>color me plz!</span></a>

    <script>
        testing = document.querySelectorAll("div + a");
        for (let i = 0; i < testing.length; i++){
            testing[i].addEventListener("mouseenter", function(){
                const x = testing[i];
                x.style.backgroundColor = "cyan";
                x.querySelector("span").style.backgroundColor = "pink";
            });
        }
    </script>
</body>
</html>

But my real problem is this:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="style-test.css">
</head>
<body>
    <nav>
        <div>
            <a href="#A1">Menu A.1</a>
            <a href="#A2">Menu A.2</a>
        </div>
        <div class="right">
            <div>
                <a href="#B1">Menu B.1</a>
                <ul>
                    <li><a href="#B1.1">B.1 Sub 1</a></li>
                    <li><a href="#B1.2">B.1 Sub 2</a></li>
                </ul>
            </div>
            <div>
                <a href="#B2"">Menu B.2</a>
                <ul>
                    <li><a href="#B2.1">B.2 Sub 1</a></li>
                    <li><a href="#B2.2">B.2 Sub 2</a></li>
                </ul>
            </div>
            <div>
                <a href="#B3">Menu B.3</a>
                <ul>
                    <li><a href="#B3.1">B.3 Sub 1 Test Longer Text</a></li>
                    <li><a href="#B3.2">B.3 Sub 2</a></li>
                </ul>
            </div>
            <div>
                <a href="#B4">Menu B.4</a>
                <ul>
                    <li><a href="#B4.1">B.4 Sub 1</a></li>
                    <li><a href="#B4.2">B.4 Sub 2</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <div style="float: left; margin-top: 1000px"></div>

    <script>
        const menu = document.querySelectorAll("nav div > a");
        console.log("Get all menu:");
        console.log(menu);
        for (let i = 0; i < menu.length; i++) {
            menu[i].addEventListener("mouseenter", function(){
                console.log("mouseEnter");
                console.log("menu[i].addEventListener:");
                console.log(menu[i]);

                //console.log(menu[i].querySelector("+ ul"));
                const subMenu = menu[i].querySelector(` + ul`);
                /* ^^^ The problem is here above ^^^ --- Everything stops here */
                console.log("OK! 'querySelector' is valid"); //<-- this won't display...

                //if(window.getComputedStyle(subMenu).getPropertyValue("display") === "block") /*Corrected*/
                if(subMenu.style.display === "block")
                {
                    console.log("subMenu.style.display === block");
                    const subMenuBox = subMenu.getBoundingClientRect();
                    const posX = (subMenuBox.left + subMenuBox.width) - window.innerWidth;
                    if(posX > 0)
                        subMenu.style.left = (-posX - 20) + "px";
                        /*padding problem (need -20): didn't .getBoundingClientRect() include padding?*/
                        // /*or just*/ subMenu.style.right = "0px";
                    console.log("When here successfully!");
                }
                else{
                    console.log("Failed...");
                }
            })
        }
    </script>
</body>
</html>

css:

body{
    color: #fff;
    font-size: 20px;
    background: #999;
    margin: 0;
}
a{
    color: #cfc;
}
nav, nav div > a, nav div{
    float: left;
}
nav{
    width: 100%;
    background: #4169E1;
    white-space: nowrap;
}
nav div{
    margin: 0em 0.2em;
}
nav div:first-child{
    margin-left: 0;
}
nav div div{
    position: relative;
    margin: 0;
}
nav a{
    color: #cff;
    display: block;
    font-weight: 600;
    text-decoration: none;
    padding: 0.5em 1em;
}
nav a:hover{
    color: #fff;
    background: #f90;
}
nav div > a + ul{
    position: absolute;
    top: 2.15em;
    float: left;
    background: #666;
    list-style-type: none;
    margin: 0;
    padding: 0;
    min-width: 180px;
    display: none;
}
nav div:hover > a + ul{
    display: block;
}
.right{
    float: right;
}

I just want to get the sibling of <a> just next to it, the <ul> after <a>, which is a + ul in css

<div>
    <a href="#B1">Menu B.1</a>
    <ul>
        <li><a href="#B1.1">B.1 Sub 1</a></li>
        <li><a href="#B1.2">B.1 Sub 2</a></li>
    </ul>
</div>

Focus here:

<script>
    const menu = document.querySelectorAll("nav div > a");

    for (let i = 0; i < menu.length; i++) {
        menu[i].addEventListener("mouseenter", function(){

            //Problem is here...
            const subMenu = menu[i].querySelector(" + ul");

            /*Corrected*/
            //if(window.getComputedStyle(subMenu).getPropertyValue("display") === "block")
            if(subMenu != null && subMenu.style.display === "block")
            {
                const subMenuBox = subMenu.getBoundingClientRect();
                const posX = (subMenuBox.left + subMenuBox.width) - window.innerWidth;
                if(posX > 0)
                    subMenu.style.left = (-posX -20) + "px";
                    /*padding problem (need -20): didn't .getBoundingClientRect() include
                    padding?*/
                    // /*or just*/ subMenu.style.right = "0px";
            }
        })
    }
</script>

Now, what I done here: First, I select all the <a> inside <nav>. Then using for() loop and put mouseenter event to all the selected <a>. When user hover over the <a>, the mouseenter know exactly which <a> was hover. Now here's the problem: I want to select the a + ul of hovered <a>.


I have tried this:

console.log(document.querySelector("menu[i] + ul"));

gives me a null value

console.log(document.querySelector(menu[i] + " + ul"));

gives me SyntaxError: 'file:///C:/Users/path/path/thisPage.html + ul' is not a valid selector

console.log(menu[i].querySelector(" + ul"));

gives me SyntaxError: '+ul' is not a valid selector

How can I fix this? What is the right thing to do with .querySelector() for continuing selection but with adjacent sibling tag?


Solution

  • On one hand, on non-ancient browsers, you can use :scope to indicate the element on which the querySelector is being called on. But querySelector will only select elements which are children of the current element, and the <ul> you want is a sibling.

    Take the nextElementSibling of the element instead, check that it exists, and check that it's a ul instead:

    for (const menu of document.querySelectorAll("nav div > a")) {
      menu.addEventListener("mouseenter", function() {
        console.log(menu, menu.nextElementSibling);
        
        const subMenu = menu.nextElementSibling;
        if (!subMenu || !subMenu.matches('ul')) {
          return;
        }
    
        if (subMenu.style.display === "block") {
          console.log("subMenu.style.display === block");
          const subMenuBox = subMenu.getBoundingClientRect();
          const posX = (subMenuBox.left + subMenuBox.width) - window.innerWidth;
          subMenu.style.left = -posX;
          console.log("When here successfully!");
        } else {
          console.log("Failed...");
        }
      })
    }
    body {
      color: #fff;
      font-size: 20px;
      background: #999;
      margin: 0;
    }
    
    a {
      color: #cfc;
    }
    
    nav,
    nav div>a,
    nav div {
      float: left;
    }
    
    nav {
      width: 100%;
      background: #4169E1;
      white-space: nowrap;
    }
    
    nav div {
      margin: 0em 0.2em;
    }
    
    nav div:first-child {
      margin-left: 0;
    }
    
    nav div div {
      position: relative;
      margin: 0;
    }
    
    nav a {
      color: #cff;
      display: block;
      font-weight: 600;
      text-decoration: none;
      padding: 0.5em 1em;
    }
    
    nav a:hover {
      color: #fff;
      background: #f90;
    }
    
    nav div>a+ul {
      position: absolute;
      top: 2.15em;
      float: left;
      background: #666;
      list-style-type: none;
      margin: 0;
      padding: 0;
      min-width: 180px;
      display: none;
    }
    
    nav div:hover>a+ul {
      display: block;
    }
    
    .right {
      float: right;
    }
    <nav>
      <div>
        <a href="#A1">Menu A.1</a>
        <a href="#A2">Menu A.2</a>
      </div>
      <div class="right">
        <div>
          <a href="#B1">Menu B.1</a>
          <ul>
            <li><a href="#B1.1">B.1 Sub 1</a></li>
            <li><a href="#B1.2">B.1 Sub 2</a></li>
          </ul>
        </div>
        <div>
          <a href="#B2">Menu B.2</a>
          <ul>
            <li><a href="#B2.1 ">B.2 Sub 1</a></li>
            <li><a href="#B2.2 ">B.2 Sub 2</a></li>
          </ul>
        </div>
        <div>
          <a href="#B3 ">Menu B.3</a>
          <ul>
            <li><a href="#B3.1 ">B.3 Sub 1 Test Longer Text</a></li>
            <li><a href="#B3.2 ">B.3 Sub 2</a></li>
          </ul>
        </div>
        <div>
          <a href="#B4 ">Menu B.4</a>
          <ul>
            <li><a href="#B4.1 ">B.4 Sub 1</a></li>
            <li><a href="#B4.2 ">B.4 Sub 2</a></li>
          </ul>
        </div>
      </div>
    </nav>
    
    <div style="float: left; margin-top: 1000px "></div>

    Because not all menus have sub-menus, you do need to test if the UL exists first before trying to do stuff with it.

    Note that subMenu.style.display === "block" is never fulfilled in the given code because the <ul>s don't have style properties directly on the elements. If they did, the test would succeed. If you're trying to see if they currently are being displayed, use window.getComputedStyle instead:

    for (const menu of document.querySelectorAll("nav div > a")) {
      menu.addEventListener("mouseenter", function() {
        console.log(menu, menu.nextElementSibling);
        
        const subMenu = menu.nextElementSibling;
        if (!subMenu || !subMenu.matches('ul')) {
          return;
        }
    
        if (window.getComputedStyle(subMenu).display === "block") {
          console.log("subMenu.style.display === block");
          const subMenuBox = subMenu.getBoundingClientRect();
          const posX = (subMenuBox.left + subMenuBox.width) - window.innerWidth;
          subMenu.style.left = -posX;
          console.log("When here successfully!");
        } else {
          console.log("Failed...");
        }
      })
    }
    body {
      color: #fff;
      font-size: 20px;
      background: #999;
      margin: 0;
    }
    
    a {
      color: #cfc;
    }
    
    nav,
    nav div>a,
    nav div {
      float: left;
    }
    
    nav {
      width: 100%;
      background: #4169E1;
      white-space: nowrap;
    }
    
    nav div {
      margin: 0em 0.2em;
    }
    
    nav div:first-child {
      margin-left: 0;
    }
    
    nav div div {
      position: relative;
      margin: 0;
    }
    
    nav a {
      color: #cff;
      display: block;
      font-weight: 600;
      text-decoration: none;
      padding: 0.5em 1em;
    }
    
    nav a:hover {
      color: #fff;
      background: #f90;
    }
    
    nav div>a+ul {
      position: absolute;
      top: 2.15em;
      float: left;
      background: #666;
      list-style-type: none;
      margin: 0;
      padding: 0;
      min-width: 180px;
      display: none;
    }
    
    nav div:hover>a+ul {
      display: block;
    }
    
    .right {
      float: right;
    }
    <nav>
      <div>
        <a href="#A1">Menu A.1</a>
        <a href="#A2">Menu A.2</a>
      </div>
      <div class="right">
        <div>
          <a href="#B1">Menu B.1</a>
          <ul>
            <li><a href="#B1.1">B.1 Sub 1</a></li>
            <li><a href="#B1.2">B.1 Sub 2</a></li>
          </ul>
        </div>
        <div>
          <a href="#B2">Menu B.2</a>
          <ul>
            <li><a href="#B2.1 ">B.2 Sub 1</a></li>
            <li><a href="#B2.2 ">B.2 Sub 2</a></li>
          </ul>
        </div>
        <div>
          <a href="#B3 ">Menu B.3</a>
          <ul>
            <li><a href="#B3.1 ">B.3 Sub 1 Test Longer Text</a></li>
            <li><a href="#B3.2 ">B.3 Sub 2</a></li>
          </ul>
        </div>
        <div>
          <a href="#B4 ">Menu B.4</a>
          <ul>
            <li><a href="#B4.1 ">B.4 Sub 1</a></li>
            <li><a href="#B4.2 ">B.4 Sub 2</a></li>
          </ul>
        </div>
      </div>
    </nav>
    
    <div style="float: left; margin-top: 1000px "></div>