0

Starting from the code found at https://www.w3schools.com/howto/howto_js_collapsible.asp, I would like to create a collapsible menu which would work also for nested content.

var coll = document.getElementsByClassName("collapsible");
var i;

for (i = 0; i < coll.length; i++) {
  coll[i].addEventListener("click", function() {
    this.classList.toggle("active");
    var content = this.nextElementSibling;
    if (content.style.maxHeight){
      content.style.maxHeight = null;
    } else {
      content.style.maxHeight = content.scrollHeight + "px";
    } 
  });
}
.collapsible {
  background-color: #777;
  color: white;
  cursor: pointer;
  padding: 18px;
  width: 100%;
  border: none;
  text-align: left;
  outline: none;
  font-size: 15px;
}

.active, .collapsible:hover {
  background-color: #555;
}

.collapsible:after {
  content: '\002B';
  color: white;
  font-weight: bold;
  float: right;
  margin-left: 5px;
}

.active:after {
  content: "\2212";
}

.content {
  padding: 0 18px;
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.2s ease-out;
  background-color: #f1f1f1;
}
<button class="collapsible">Open Collapsible</button>
<div class="content">
<button class="collapsible">Open Collapsible</button>
<div class="content">
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
<button class="collapsible">Open Collapsible</button>
<div class="content">
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
</div>

The code above works for the first (root) collapsible.

However, when the child collapsible are expanded, there is no space enough for seeing their content.

Only if they are kept opened, closing the root collapsible and reopening it will show the child content correctly.

I know the problem is in that when the root collapsible is expanded, its content maxHeight is set to scrollHeight + "px";, and this will be the height of the child collapsible which are still closed.

How can I make the maxHeight of the collapsibles dynamically change as their children collapsible expand?

umbe1987
  • 2,894
  • 6
  • 35
  • 63

1 Answers1

1

You should check and modify max-height for elements on child opening and closing (partial code).

for (i = 0; i < coll.length; i++) {
  coll[i].addEventListener("click", function() {
    this.classList.toggle("active");
    var content = this.nextElementSibling;
    if (content.style.maxHeight){
      content.style.maxHeight = null;
    } else {
      content.style.maxHeight = content.scrollHeight + "px";
    }
    // from last to first to recalculate parent height after child
    for (var j = coll.length - 1; j >= 0;  j--) {
      if (coll[j].classList.contains('active')) {
        console.log(j, coll[j].classList);
        var c2 = coll[j].nextElementSibling;
        console.log(c2);
        c2.style.maxHeight = null;
        c2.style.maxHeight = c2.scrollHeight + "px";
      }
    }
  });
}

But code above will work if css transition is disabled (because js triggers immediately before transition completed).

So, if transition is necessary you can add timeout to check for modification after transition ends. Something like this:

var coll = document.getElementsByClassName("collapsible");
var i;

var checkCollapsible = function() {
  for (var j = coll.length - 1; j >= 0;  j--) {
      if (coll[j].classList.contains('active')) {
        console.log(j, coll[j].classList);
        var c2 = coll[j].nextElementSibling;
        console.log(c2);
        c2.style.maxHeight = null;
        c2.style.maxHeight = c2.scrollHeight + "px";
      }
    }
};

for (i = 0; i < coll.length; i++) {
  coll[i].addEventListener("click", function() {
    this.classList.toggle("active");
    var content = this.nextElementSibling;
    if (content.style.maxHeight){
      content.style.maxHeight = null;
    } else {
      content.style.maxHeight = content.scrollHeight + "px";
    } 
    window.setTimeout(checkCollapsible, 200);
  });
}

https://jsfiddle.net/6yn0mewz/3/

There are also bunch of transitionend events available. So for webkit you may use something like (partial code)

for (i = 0; i < coll.length; i++) {
  coll[i].addEventListener("click", function() {
    this.classList.toggle("active");
    var content = this.nextElementSibling;
    if (content.style.maxHeight){
      content.style.maxHeight = null;
    } else {
      content.style.maxHeight = content.scrollHeight + "px";
    }
   content.addEventListener('webkitTransitionEnd', checkCollapsible);
  });
}

https://jsfiddle.net/6yn0mewz/4/

See CSS3 transition events and TransitionEnd Event not firing? for examples.

Sergiy T.
  • 1,433
  • 1
  • 23
  • 25
  • Thanks. However, the effect seems a bit ugly to me (the transition is not smooth and the lag is considerable).I am thinking about traversing back the parents with [`closest()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest) and updating the `maxHeight` property... – umbe1987 Jun 19 '20 at 12:48