matteodelabre matteodelabre - 4 months ago 9
Javascript Question

How can I prevent a flex container from overflowing/underflowing?

I have a flexbox container that can be resized by dragging. How can I prevent that container from overflowing (getting too small and starting to hide some items) or underflowing (getting too large and showing blank space)?

In the following example, the container should stop shrinking when all items reach

18px
height (except the last one), and stop expanding when all items reach their maximal heights (no blank space should appear).



const resizable = document.querySelector('.flex');
let startHeight = NaN;
let startY = NaN;

resizable.addEventListener('mousedown', e => {
startHeight = resizable.getBoundingClientRect().height;
startY = e.pageY;
});

window.addEventListener('mousemove', e => {
if (isNaN(startY) || isNaN(startHeight)) {
return;
}

resizable.style.height = (startHeight + e.pageY - startY) + 'px';
});

window.addEventListener('mouseup', () => {
startHeight = startY = NaN;
});

.flex {
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid black;
cursor: ns-resize;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

.item-1 {
background: red;
height: 40px;
max-height: 60px;
flex: 1;
}

.item-2 {
background: blue;
flex: none;
}

.item-3 {
background: green;
height: 50px;
max-height: 50px;
flex: 1;
}

.item-4 {
background: yellow;
height: 30px;
flex: none;
}

<div class="flex">
<div class="item-1">
Item 1
</div>
<div class="item-2">
Item 2
</div>
<div class="item-3">
Item 3
</div>
<div class="item-4">
Item 4
</div>
</div>





A pure CSS solution would be preferred. The solution should allow shrinking the container so that all the items are at their minimal size, and expanding it so that all of them occupy their maximal size. Hardcoding a minimal and maximal height in the container's CSS is not acceptable. I am seeking a generic solution.

Answer

I don't think a pure CSS solution would be possible here since you set the height absolutely using JavaScript.

You could just add a guard like I've done in the snippet below. You could also calculate the bottom height when the resizable size is zero and when it's huge and introduce boundaries based on that (probably better performance but bad with dom changes)

const resizable = document.querySelector('.flex');
const lastChild = resizable.querySelector(':last-child');
const resizableBorderBottom = 1;  // hardcoded - could be computed too
let startHeight = NaN;
let startY = NaN;

resizable.addEventListener('mousedown', e => {
  /* 
   * here we assume that the bottom of the last child
   * is the same as the bottom of the resizable for simplicity
   */
  startHeight = resizable.getBoundingClientRect().height;
  startY = e.pageY;
});

window.addEventListener('mousemove', e => {
  if (isNaN(startY) || isNaN(startHeight)) {
    return;
  }
  const lastHeight = resizable.style.height;
  resizable.style.height = ((startHeight + e.pageY - startY) | 0) + 'px';
  const lastChildBottom = lastChild.getBoundingClientRect().bottom | 0;
  const resizableBottom = (resizable.getBoundingClientRect().bottom | 0) - resizableBorderBottom;
  // check if we need to revert the change
  if (lastChildBottom !== resizableBottom) {
    resizable.style.height = lastHeight;
  }
});

window.addEventListener('mouseup', () => {
  startHeight = startY = NaN;
});
.flex {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid black;
  cursor: ns-resize;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.item-1 {
  background: red;
  height: 40px;
  max-height: 60px;
  flex: 1;
}

.item-2 {
  background: blue;
  flex: none;
}

.item-3 {
  background: green;
  height: 50px;
  max-height: 50px;
  flex: 1;
}

.item-4 {
  background: yellow;
  height: 30px;
  flex: none;
}
<div class="flex">
  <div class="item-1">
    Item 1
  </div>
  <div class="item-2">
    Item 2
  </div>
  <div class="item-3">
    Item 3
  </div>
  <div class="item-4">
    Item 4
  </div>
</div>