JavaScript question

Discuss how to use and promote Web standards with the Mozilla Gecko engine.
Locked
Bethrezen
Posts: 445
Joined: September 13th, 2003, 11:56 am

JavaScript question

Post by Bethrezen »

Hi all

So I have a number of details/summary tags and I want to create an expand all / contract all button however for some reason I can’t seem to get it working right.

So first I did this

Code: Select all

<details open>
  <summary>item 1</summary>

  <p>some content</p>
</details>

var has_open = document.querySelector("details").hasAttribute("open");

alert(document.querySelector("details > summary").innerHTML);
and everything worked as expected and I though grate it shouldn’t be to tough to modify this to do the same thing for more then 1 details element so I modified the code as follows

Code: Select all

<details>
  <summary>item 1</summary>
</details>

<details open>
  <summary>item 2</summary>
</details>

<details open>
  <summary>item 3</summary>
</details>

var has_open = document.querySelectorAll("details").hasAttribute("open");

for (i = 0; i < has_open.length; i++)
{
  alert(document.querySelectorAll("details > summary")[i].innerHTML);
}
However much to my irritation when I try running this I get the following error
TypeError: document.querySelectorAll(...).hasAttribute is not a function
so my question is what am I missing because as far I can tell the above code should work and should return a list of all details elements that have the open property and should then alert item 2, item 3 as those are the ones that have the open property.

why is it that if I want to check just a single details element and alert out the content of the summary tag everything works fine but when I try to do the same thing for multiple details elements it wont work properly.

in any event here is what I have so far

Code: Select all

<button onclick="toggle()">expand all</button>

  // Get the div that has the .show class then get a list of all detail elements within it
  var details = document.querySelector('.show').querySelectorAll("details");

  //loop through the list of details elements one at a time starting at index 0.
  for (i = 0; i < details.length; i++)
  {
    // Get a list of detail elements that have the open attribute
    var has_open = details[i].hasAttribute("open");
  }

  if(has_open) //if any of the details elements have the open attribute
  {
    //loop through the list of detail elements one at a time starting at index 0.
    for (i = 0; i < details.length; i++)
    {
      // for each detail element that have the open attribute remove the open attribute
      details[i].removeAttribute("open");

      // Get the expand all / collapse all button and toggle its text
      document.querySelector("button").innerHTML = "expand all"
    }
  }

  else //if any of the detail elements don't have the open attribute
  {
    //loop through the list of detail elements one at a time starting at index 0.
    for (i = 0; i < details.length; i++)
    {
      // for each detail element that does not have the open attribute add the open attribute.
      details[i].setAttribute("open", "true");

      // Get the expand all / collapse all button and toggle its text
      document.querySelector("button").innerHTML = "collapse all"
    }
  }
now this sort of works but its buggy if tap the button to expand all the details elements and then manually close some of the details elements and then tap the button to close all the details elements instead of removing the open property from the remaining details elements it does the opposite and it adds the open property to the details elements that I closed manually so clearly something isn't right
User avatar
mtalbot
Posts: 50
Joined: October 13th, 2015, 5:21 am

Re: JavaScript question

Post by mtalbot »

If you take a look at the page Document.querySelectorAll() you will see that you can't access the returned result like it is an array. It is a node list so you need to iterate through it like they show in this example:

Code: Select all

const highlightedItems = userList.querySelectorAll(".highlighted");

highlightedItems.forEach(function(userItem) {
  deleteUser(userItem);
});
Naturally you'll need to change the deleteUser(userItem); to what ever you want/need to do to the element.

I hope this helps.
morat
Posts: 6403
Joined: February 3rd, 2009, 6:29 pm

Re: JavaScript question

Post by morat »

You can use the first details element open status to control the flow of the toggle function.

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Example</title>
  <script>
  function toggle() {
    var details = document.querySelectorAll("details");
    var flag = details[0].open;
    if (flag) {
      for (var i = 0; i < details.length; i++) {
        details[i].removeAttribute("open");
      }
    } else {
      for (var i = 0; i < details.length; i++) {
        details[i].setAttribute("open", "");
      }
    }
  }
  </script>
</head>
<body>
<button onclick="toggle()">toggle</button>
<details>
  <summary>item 1</summary>
  note 1
</details>
<details>
  <summary>item 2</summary>
  note 2
</details>
<details>
  <summary>item 3</summary>
  note 3
</details>
</body>
</html>
Bethrezen
Posts: 445
Joined: September 13th, 2003, 11:56 am

Re: JavaScript question

Post by Bethrezen »

morat wrote:You can use the first details element open status to control the flow of the toggle function.
unfortunately that doesn't work see when I tap the button to open all the details elements and then manually close the first details element and then I tap the button again it should close the remaining details elements however it doesn't it just adds the open attribute to the first details element which is exactly the problem I was getting before as you can see in this example.

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>

<button onclick="toggle()">expand all</button>

<details>
  <summary>item 1</summary>
  note 1
</details>

<details>
  <summary>item 2</summary>
  note 2
</details>

<details>
  <summary>item 3</summary>
  note 3
</details>

<script>

function toggle()
{
  var details = document.querySelectorAll("details");
  var open = details[0].open;

  if(open)
  {
    for (var i = 0; i < details.length; i++)
    {
      details[i].removeAttribute("open");
      document.querySelector("button").innerHTML = "expand all"
    }
  }

  else
  {
    for (var i = 0; i < details.length; i++)
    {
      details[i].setAttribute("open", "");
      document.querySelector("button").innerHTML = "collapse all"
    }
  }
}

</script>
</body>
</html>
This is why I was trying to check if any of the details elements have the open attribute because if any of the details elements have open attribute then the condition in the if statement should return true and therefore close the remaining details elements even if I have closed one or more manually.
mtalbot wrote:If you take a look at the page Document.querySelectorAll() you will see that you can't access the returned result like it is an array. It is a node list so you need to iterate through it like they show in this example:
unfortunately this dosn't work because when I tried doing as you suggested I just get the following error.
TypeError: document.querySelectorAll(...).hasAttribute is not a function.
As you can see in this example.

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>

<button onclick="toggle()">toggle</button>

<details>
  <summary>item 1</summary>
  note 1
</details>

<details>
  <summary>item 2</summary>
  note 2
</details>

<details>
  <summary>item 3</summary>
  note 3
</details>

<script>

function toggle()
{
  var has_open = document.querySelectorAll("details").hasAttribute("open");

  has_open.forEach(alert);

  function alert()
  {
    alert(document.querySelector("details > summary").innerHTML);
  }
}

</script>
</body>
</html>
again I don't understand why this doesn't want to work because because again it should be returning a list all all details elements that have the open attribute I should then be able to loop through each one and alert the contents of there summary tags to let me see what is being returned so I can make sure its working correctly and returning the expected result.
morat
Posts: 6403
Joined: February 3rd, 2009, 6:29 pm

Re: JavaScript question

Post by morat »

You can use the button text content property to control the flow of the toggle function.

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Example</title>
  <script>
  function toggle() {
    var button = document.querySelector("button");
    var details = document.querySelectorAll("details");
    var flag = button.textContent === "collapse all";
    if (flag) {
      for (var i = 0; i < details.length; i++) {
        details[i].removeAttribute("open");
      }
      button.textContent = "expand all";
    } else {
      for (var i = 0; i < details.length; i++) {
        details[i].setAttribute("open", "");
      }
      button.textContent = "collapse all";
    }
  }
  </script>
</head>
<body>
<button onclick="toggle()">expand all</button>
<details>
  <summary>item 1</summary>
  note 1
</details>
<details>
  <summary>item 2</summary>
  note 2
</details>
<details>
  <summary>item 3</summary>
  note 3
</details>
</body>
</html>
Bethrezen
Posts: 445
Joined: September 13th, 2003, 11:56 am

Re: JavaScript question

Post by Bethrezen »

After a bit more tinkering I think I sussed it out instead of doing

Code: Select all

var has_open = document.querySelectorAll("details").hasAttribute("open");
which caused the aforementioned error I remembered that querySelectorAll would accept any valid CSS selector so I was able to get a list of all the details elements that have the open attribute like this.

Code: Select all

var has_open = document.querySelectorAll('details[open=""]');
so doing this

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>

<button onclick="toggle()">toggle</button>

<details>
  <summary>item 1</summary>
  note 1
</details>

<details>
  <summary>item 2</summary>
  note 2
</details>

<details>
  <summary>item 3</summary>
  note 3
</details>

<script>

function toggle()
{
  var has_open = document.querySelectorAll('details[open=""]');

  for (var i = 0; i < details.length; i++)
  {
    alert(document.querySelector("details > summary").innerHTML);
  }
}

</script>
</body>
</html>
Now works correctly, it no longer gives errors and only alerts the contents of the summary tag for the details elements that have the open attribute as expected.

Which of course just left me to figure out the right condition to use in the if statement and on that score I actually came up with a similar solution, I probably don't need the condition on the else but I figure I put it in just to be thorough.

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>

<details>
  <summary>item 1</summary>
  note 1
</details>

<details>
  <summary>item 2</summary>
  note 2
</details>

<details>
  <summary>item 3</summary>
  note 3
</details>

<script>

function toggle()
{
  var button = document.querySelector("button");
  var details = document.querySelectorAll(".show details");
  var has_open = document.querySelectorAll('.show details[open=""]');

  if(button.innerHTML == "collapse all")
  {
    for (i = 0; i < has_open.length; i++)
    {
      has_open[i].removeAttribute("open");
      button.innerHTML = "expand all";
    }
  }

  else if(button.innerHTML == "expand all")
  {
    for (i = 0; i < details.length; i++)
    {
      details[i].setAttribute("open", "");
      button.innerHTML = "collapse all";
    }
  }
}

</script>
</body>
</html>
Although thinking about it I'm tempted to just use 2 buttons it would be somewhat simpler and less prone to problems.
Locked