javascript 当外部 div 的大小发生变化时,可滚动的 div 会粘在底部

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/34213227/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-28 17:23:50  来源:igfitidea点击:

Scrollable div to stick to bottom, when outer div changes in size

javascriptcssreactjsflexbox

提问by Stepan Parunashvili

Here is an example chat app ->

这是一个示例聊天应用程序 ->

The idea here is to have the .messages-containertake up as much of the screen as it can. Within .messages-container, .scrollholds the list of messages, and in case there are more messages then the size of the screen, scrolls.

这里的想法是.messages-container尽可能多地占用屏幕。在.messages-container, 中.scroll保存消息列表,如果有更多消息,则屏幕大小会滚动。

Now, consider this case:

现在,考虑这个案例:

  1. The user scrolls to the bottom of the conversation
  2. The .text-input, dynamically gets bigger
  1. 用户滚动到对话底部
  2. .text-input,动态地变大

Now, instead of the user staying scrolled to the bottom of the conversation, the text-input increases, and they no longer see the bottom.

现在,用户不再滚动到对话底部,而是文本输入增加,他们不再看到底部。

One way to fix it, if we are using react, calculate the height of text-input, and if anything changes, let .messages-container know

一种解决方法,如果我们使用 react,计算文本输入的高度,如果有任何变化,让 .messages-container 知道

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

But, this causes visible performance issues, and it's sad to be passing messages around like this.

但是,这会导致明显的性能问题,并且像这样传递消息令人难过。

Is there a better way? Could I use css in such a way, to express that when .text-input-increases, I want to essentially shift upall of .messages-container

有没有更好的办法?我可以以这种方式使用 css 来表达当 .text-input-increases 时,我想要基本上shift up所有的 .messages-container

采纳答案by Ason

2:nd revision of this answer

2:此答案的第 2 次修订

Your friend here is flex-direction: column-reverse;which does all you ask while align the messages at the bottom of the message container, just like for example Skype and many other chat apps do.

你在这里的朋友是flex-direction: column-reverse;在对齐消息容器底部的消息的同时完成你的所有要求,就像 Skype 和许多其他聊天应用程序所做的那样。

.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%; }

The downside with flex-direction: column-reverse;is a bug in IE/Edge/Firefox, where the scrollbar doesn't show, which your can read more about here: Flexbox column-reverse and overflow in Firefox/IE

缺点flex-direction: column-reverse;是 IE/Edge/Firefox 中的一个错误,滚动条不显示,您可以在此处阅读更多信息:Flexbox column-reverse and overflow in Firefox/IE

The upsideis you have ~ 90% browser support on mobile/tablets and ~ 65% for desktop, and counting as the bug gets fixed, ...and there is a workaround.

好处是你在移动设备/平板电脑上有大约 90% 的浏览器支持,对于桌面设备有大约 65% 的浏览器支持,并且随着错误得到修复而计算,......并且有一个解决方法。

// 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));
}

In the below code snippet I've added the 2 functions from above, to make IE/Edge/Firefox behave in the same way flex-direction: column-reverse;does.

在下面的代码片段中,我添加了上面的 2 个函数,以使 IE/Edge/Firefox 以相同的方式运行flex-direction: column-reverse;

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>



Side note 1: The detection method is not fully tested, but it should work on newer browsers.

旁注 1:检测方法没有经过全面测试,但它应该适用于较新的浏览器。

Side note 2: Attach a resize event handler for the chat-input might be more efficient then calling the updateScroll function.

旁注 2:为聊天输入附加调整大小事件处理程序可能比调用 updateScroll 函数更有效。

Note: Credits to HaZardouSfor reusing his html structure

注意:感谢HaZardouS重用他的 html 结构

回答by DoctorDestructo

You just need one CSS rule set:

你只需要一套 CSS 规则:

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

That's it, you're done!

就是这样,你完成了!

How it works:First, it vertically flips the container element so that the top becomes the bottom (giving us the desired scroll orientation), then it flips the content element so that the messages won't be upside down.

工作原理:首先,它垂直翻转容器元素,使顶部变为底部(为我们提供所需的滚动方向),然后翻转内容元素,使消息不会颠倒。

This approach works in all modern browsers. It does have a strange side effect, though: when you use a mouse wheel in the message box, the scroll direction is reversed. This can be fixed with a few lines of JavaScript, as shown below.

这种方法适用于所有现代浏览器。但是,它确实有一个奇怪的副作用:当您在消息框中使用鼠标滚轮时,滚动方向会反转。这可以通过几行 JavaScript 来解决,如下所示。

Here's a demo and a fiddleto play with:

这是一个演示和一个小提琴

//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>

回答by hazardous

Please try the following fiddle - https://jsfiddle.net/Hazardous/bypxg25c/. Although the fiddle is currently using jQuery to grow/resize the text area, the crux is in the flex related styles used for the messages-container and input-container classes -

请尝试以下小提琴 - https://jsfiddle.net/Hazardous/bypxg25c/。尽管小提琴目前正在使用 jQuery 来增大/调整文本区域的大小,但关键在于用于消息容器和输入容器类的 flex 相关样式 -

.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;
}

The flex-shrink value is set to 1 for .messages-container and 0 for .input-container. This ensures that messages-container shrinks when there is a reallocation of size.

.messages-container 的 flex-shrink 值设置为 1,.input-container 的 flex-shrink 值设置为 0。这确保在重新分配大小时消息容器会缩小。

回答by Jamie Barker

I've moved text-inputwithin messages, absolute positioned it to the bottom of the container and given messagesenough bottom padding to space accordingly.

我搬到text-inputmessages,其绝对定位在容器的底部,并给予messages相应足够的底部填充空间。

Run some code to add a class to conversation, which changes the height of text-inputand bottom padding of messagesusing a nice CSS transition animation.

运行一些代码向 中添加一个类conversation,这会更改使用漂亮的 CSS 过渡动画的高度text-input和底部填充messages

The JavaScript runs a "scrollTo" function at the same time as the CSS transition is running to keep the scroll at the bottom.

JavaScript 在 CSS 转换运行的同时运行“scrollTo”函数以保持滚动在底部。

When the scroll comes off the bottom again, we remove the class from conversation

当滚动再次离开底部时,我们从 conversation

Hope this helps.

希望这可以帮助。

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

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>

回答by J. Mark Stevens

You write;

你写;

Now, consider this case:

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

Wouldn't the method that dynamically sets the .text-input be the logical place to fire this.props.onResize().

动态设置 .text-input 的方法不是触发 this.props.onResize() 的逻辑位置吗?

回答by odlh

To whom it may concern,

敬启者,

The answers above did not suffice my question.

上面的答案不足以解决我的问题。

The solution I found was to make my innerWidth and innerHeight variable constant - as the innerWidth of the browser changes on scroll to adapt for the scrollbar.

我找到的解决方案是使我的 innerWidth 和 innerHeight 变量保持不变 - 因为浏览器的 innerWidth 在滚动时会发生变化以适应滚动条。

var innerWidth = window.innerWidth
var innerHeight = window.innerHeight

OR FOR REACT

this.setState({width: window.innerWidth, height: window.innerHeight})

In other words, to ignore it, you must make everything constant as if it were never scrolling. Do remember to update these on Resize / Orientation Change !

换句话说,要忽略它,您必须使所有内容保持不变,就好像它从不滚动一样。请记住在调整大小/方向更改时更新这些!

Oscar

奥斯卡