web-dev-qa-db-fra.com

Div scrollable pour coller au fond, lorsque la taille de la div externe change

Voici un exemple d'application de chat ->

L'idée ici est d'avoir le .messages-container occupe autant d'écran que possible. Dans .messages-container, .scroll contient la liste des messages et, s'il y a plus de messages que la taille de l'écran, défile.

Maintenant, considérons ce cas:

  1. L'utilisateur fait défiler vers le bas de la conversation
  2. Le .text-input, s'agrandit dynamiquement

Maintenant, au lieu que l'utilisateur reste en défilement vers le bas de la conversation, la saisie de texte augmente et il ne voit plus le bas.

Une façon de résoudre ce problème, si nous utilisons React, calculons la hauteur de la saisie de texte et, si quelque chose change, faites savoir à .messages-container

componentDidUpdate() {
  window.setTimeout(_ => {
    const newHeight = this.calcHeight();
    if (newHeight !== this._oldHeight) {
      this.props.onResize();
    }
    this._oldHeight = newHeight;
  });
}

Mais cela entraîne des problèmes de performances visibles et il est triste de faire passer des messages comme celui-ci.

Y a-t-il une meilleure façon? Puis-je utiliser css de cette manière, pour exprimer que lorsque .text-input-augmente, je veux essentiellement shift up tous les .messages-container

36
Stepan Parunashvili

2: nd révision de cette réponse

Votre ami ici est flex-direction: column-reverse; qui fait tout ce que vous demandez tout en alignant les messages en bas du conteneur de messages, comme par exemple Skype et de nombreuses autres applications de chat.

.chat-window{
  display:flex;
  flex-direction:column;
  height:100%;
}
.chat-messages{
  flex: 1;
  height:100%;
  overflow: auto;
  display: flex;
  flex-direction: column-reverse;
}

.chat-input { border-top: 1px solid #999; padding: 20px 5px }
.chat-input-text { width: 60%; min-height: 40px; max-width: 60%; }

L'inconvénient avec flex-direction: column-reverse; est un bogue dans IE/Edge/Firefox, où la barre de défilement ne s'affiche pas, sur laquelle vous pouvez en lire plus ici: Flexbox column-reverse and overflow in Firefox/IE

L'avantage est que vous avez ~ 90% de prise en charge du navigateur sur mobile/tablettes et ~ 65% pour le bureau, et en comptant que le bug est corrigé, ... et il existe une solution de contournement.

// scroll to bottom
function updateScroll(el){
  el.scrollTop = el.scrollHeight;
}
// only shift-up if at bottom
function scrollAtBottom(el){
  return (el.scrollTop + 5 >= (el.scrollHeight - el.offsetHeight));
}

Dans l'extrait de code ci-dessous, j'ai ajouté les 2 fonctions ci-dessus, pour que IE/Edge/Firefox se comportent de la même manière flex-direction: column-reverse; Est-ce que.

function addContent () {
  var msgdiv = document.getElementById('messages');
  var msgtxt = document.getElementById('inputs');
  var atbottom = scrollAtBottom(msgdiv);

  if (msgtxt.value.length > 0) {
    msgdiv.innerHTML += msgtxt.value + '<br/>';
    msgtxt.value = "";
  } else {
    msgdiv.innerHTML += 'Long long content ' + (tempCounter++) + '!<br/>';
  }
  
  /* if at bottom and is IE/Edge/Firefox */
  if (atbottom && (!isWebkit || isEdge)) {
    updateScroll(msgdiv);
  }
}

function resizeInput () {
  var msgdiv = document.getElementById('messages');
  var msgtxt = document.getElementById('inputs');
  var atbottom = scrollAtBottom(msgdiv);

  if (msgtxt.style.height == '120px') {
    msgtxt.style.height = 'auto';
  } else {
    msgtxt.style.height = '120px';
  }
  
  /* if at bottom and is IE/Edge/Firefox */
  if (atbottom && (!isWebkit || isEdge)) {
    updateScroll(msgdiv);
  }
}


/* fix for IE/Edge/Firefox */
var isWebkit = ('WebkitAppearance' in document.documentElement.style);
var isEdge = ('-ms-accelerator' in document.documentElement.style);
var tempCounter = 6;

function updateScroll(el){
  el.scrollTop = el.scrollHeight;
}
function scrollAtBottom(el){
  return (el.scrollTop + 5 >= (el.scrollHeight - el.offsetHeight));
}
html, body { height:100%; margin:0; padding:0; }

.chat-window{
  display:flex;
  flex-direction:column;
  height:100%;
}
.chat-messages{
  flex: 1;
  height:100%;
  overflow: auto;
  display: flex;
  flex-direction: column-reverse;
}

.chat-input { border-top: 1px solid #999; padding: 20px 5px }
.chat-input-text { width: 60%; min-height: 40px; max-width: 60%; }


/* temp. buttons for demo */
button { width: 12%; height: 44px; margin-left: 5%; vertical-align: top; }

/* begin - fix for hidden scrollbar in IE/Edge/Firefox */
.chat-messages-text{ overflow: auto; }
@media screen and (-webkit-min-device-pixel-ratio:0) {
  .chat-messages-text{ overflow: visible; }
  /*  reset Edge as it identifies itself as webkit  */
  @supports (-ms-accelerator:true) { .chat-messages-text{ overflow: auto; } }
}
/* hide resize FF */
@-moz-document url-prefix() { .chat-input-text { resize: none } }
/* end - fix for hidden scrollbar in IE/Edge/Firefox */
<div class="chat-window">
  <div class="chat-messages">
    <div class="chat-messages-text" id="messages">
      Long long content 1!<br/>
      Long long content 2!<br/>
      Long long content 3!<br/>
      Long long content 4!<br/>
      Long long content 5!<br/>
    </div>
  </div>
  <div class="chat-input">
    <textarea class="chat-input-text" placeholder="Type your message here..." id="inputs"></textarea>
    <button onclick="addContent();">Add msg</button>
    <button onclick="resizeInput();">Resize input</button>
  </div>
</div>

Note 1: La méthode de détection n'est pas entièrement testée, mais elle devrait fonctionner sur les nouveaux navigateurs.

Note latérale 2: Attacher un gestionnaire d'événements de redimensionnement pour l'entrée de discussion peut être plus efficace que d'appeler la fonction updateScroll.

Remarque: Crédits à HaZardouS pour la réutilisation de sa structure html

21
LGSon

Vous n'avez besoin que d'un ensemble de règles CSS:

.messages-container, .scroll {transform: scale(1,-1);}

Ça y est, vous avez terminé!

Comment cela fonctionne: Tout d'abord, il retourne verticalement l'élément conteneur de sorte que le haut devienne le bas (nous donnant l'orientation de défilement souhaitée), puis il retourne le élément de contenu afin que les messages ne soient pas à l'envers.

Cette approche fonctionne dans tous les navigateurs modernes. Cependant, cela a un effet secondaire étrange: lorsque vous utilisez une molette de la souris dans la boîte de message, le sens de défilement est inversé. Cela peut être résolu avec quelques lignes de JavaScript, comme indiqué ci-dessous.

Voici une démo et un violon pour jouer avec:

//Reverse wheel direction
document.querySelector('.messages-container').addEventListener('wheel', function(e) {
  if(e.deltaY) {
    e.preventDefault();
    e.currentTarget.scrollTop -= parseFloat(getComputedStyle(e.currentTarget).getPropertyValue('font-size')) * (e.deltaY < 0 ? -1 : 1) * 2;
  }
});

//The rest of the JS just handles the test buttons and is not part of the solution
send = function() {
  var inp = document.querySelector('.text-input');
  document.querySelector('.scroll').insertAdjacentHTML('beforeend', '<p>' + inp.value);
  inp.value = '';
  inp.focus();
}
resize = function() {
  var inp = document.querySelector('.text-input');
  inp.style.height = inp.style.height === '50%' ? null : '50%';
}
html,body {height: 100%;margin: 0;}
.conversation {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.messages-container {
  flex-shrink: 10;
  height: 100%;
  overflow: auto;
}
.messages-container, .scroll {transform: scale(1,-1);}
.text-input {resize: vertical;}
<div class="conversation">
  <div class="messages-container">
    <div class="scroll">
      <p>Message 1<p>Message 2<p>Message 3<p>Message 4<p>Message 5
      <p>Message 6<p>Message 7<p>Message 8<p>Message 9<p>Message 10
    </div>
  </div>
  <textarea class="text-input" autofocus>Your message</textarea>
  <div>
    <button id="send" onclick="send();">Send input</button>
    <button id="resize" onclick="resize();">Resize input box</button>
  </div>
</div>
11
DoctorDestructo

Veuillez essayer le violon suivant - https://jsfiddle.net/Hazardous/bypxg25c/ . Bien que le violon utilise actuellement jQuery pour agrandir/redimensionner la zone de texte, le nœud est dans les styles associés à flex utilisés pour les classes messages-container et input-container -

.messages-container{
  order:1;
  flex:0.9 1 auto;
  overflow-y:auto;
  display:flex;
  flex-direction:row;
  flex-wrap:nowrap;
  justify-content:flex-start;
  align-items:stretch;
  align-content:stretch;
}

.input-container{
  order:2;
  flex:0.1 0 auto;
}

La valeur de flex-shrink est définie sur 1 pour .messages-container et 0 pour .input-container. Cela garantit que le conteneur de messages diminue en cas de réaffectation de taille.

2
hazardous

J'ai déménagé text-input dans messages, absolu positionné au bas du conteneur et messages suffisamment de rembourrage inférieur pour espacer en conséquence.

Exécutez du code pour ajouter une classe à conversation, ce qui modifie la hauteur de text-input et le remplissage inférieur de messages en utilisant une animation de transition CSS agréable.

Le JavaScript exécute une fonction "scrollTo" en même temps que la transition CSS s'exécute pour garder le défilement en bas.

Lorsque le défilement revient à nouveau en bas, nous supprimons la classe de conversation

J'espère que cela t'aides.

https://jsfiddle.net/cnvzLfso/5/

var doScollCheck = true;
var objConv = document.querySelector('.conversation');
var objMessages = document.querySelector('.messages');
var objInput = document.querySelector('.text-input');

function scrollTo(element, to, duration) {
  if (duration <= 0) {
    doScollCheck = true;
    return;
  }
  var difference = to - element.scrollTop;
  var perTick = difference / duration * 10;

  setTimeout(function() {
    element.scrollTop = element.scrollTop + perTick;
    if (element.scrollTop === to) {
      doScollCheck = true;
      return;
    }
    scrollTo(element, to, duration - 10);
  }, 10);
}

function resizeInput(atBottom) {
  var className = 'bigger',
    hasClass;
  if (objConv.classList) {
    hasClass = objConv.classList.contains(className);
  } else {
    hasClass = new RegExp('(^| )' + className + '( |$)', 'gi').test(objConv.className);
  }
  if (atBottom) {
    if (!hasClass) {
      doScollCheck = false;
      if (objConv.classList) {
        objConv.classList.add(className);
      } else {
        objConv.className += ' ' + className;
      }
      scrollTo(objMessages, (objMessages.scrollHeight - objMessages.offsetHeight) + 50, 500);
    }
  } else {
    if (hasClass) {
      if (objConv.classList) {
        objConv.classList.remove(className);
      } else {
        objConv.className = objConv.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
      }
    }
  }
}

objMessages.addEventListener('scroll', function() {
  if (doScollCheck) {
    var isBottom = ((this.scrollHeight - this.offsetHeight) === this.scrollTop);
    resizeInput(isBottom);
  }
});
html,
body {
  height: 100%;
  width: 100%;
  background: white;
}
body {
  margin: 0;
  padding: 0;
}
.conversation {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
  position: relative;
}
.messages {
  overflow-y: scroll;
  padding: 10px 10px 60px 10px;
  -webkit-transition: padding .5s;
  -moz-transition: padding .5s;
  transition: padding .5s;
}
.text-input {
  padding: 10px;
  -webkit-transition: height .5s;
  -moz-transition: height .5s;
  transition: height .5s;
  position: absolute;
  bottom: 0;
  height: 50px;
  background: white;
}
.conversation.bigger .messages {
  padding-bottom: 110px;
}
.conversation.bigger .text-input {
  height: 100px;
}
.text-input input {
  height: 100%;
}
<div class="conversation">
  <div class="messages">
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is a message content
    </p>
    <p>
      This is the last message
    </p>
    <div class="text-input">
      <input type="text" />
    </div>
  </div>
</div>
1
Jamie Barker

Vous écrivez;

Now, consider this case:

    The user scrolls to the bottom of the conversation
    The .text-input, dynamically gets bigger

La méthode qui définit dynamiquement l'entrée .text ne serait-elle pas l'endroit logique pour déclencher this.props.onResize ().

0
J. Mark Stevens