admin管理员组

文章数量:1335442

I'm building a chat interface for a website. When it first loads, the most recent messages are displayed, with the newest at the bottom, as is mon with most chat apps.

As the user scrolls up, more messages are loaded via our API, and inserted above the existing messages.

As you might expect, the chat bubbles are styled <div>s inside a container <div> that has overflow-y: auto and a set height.

Currently what I'm doing is noting the top message <div>, loading the older messages above it, and then repositioning the view to try and put the user back to almost where they left off, but it's tricky, especially when the chat bubbles contain dynamically loading elements (embedded images, etc). (I do not know the height of these bubbles before the browser renders them.)

In an ideal world, I would like to find a way to insert the messages above, without causing the scrolled position to move at all, so that the user doesn't lose track of where they are in the stream of messages.

I've never heard of such a thing, but it would be cool if there was a way to tell the div to stretch itself in the upward direction, rather than causing it to push the existing messages down further.

Any advice would be appreciated.

Here is a JSFiddle that shows the basic problem. It simulates my chat interface, by loading 30 chat messages when the page loads, and then puts you at the bottom of the message list. As you scroll to the top, another 30 are loaded, but you'll see that this is jarring to the UX because you instantly lose your place.

UPDATE: Thanks to Richard's answer, with a technique based on requestAnimationFrame to re-align the viewport. (For my use of it, I utilized the scrollTop of my scrollable div, rather than window.scrollTo as shown in his example.)

I'm building a chat interface for a website. When it first loads, the most recent messages are displayed, with the newest at the bottom, as is mon with most chat apps.

As the user scrolls up, more messages are loaded via our API, and inserted above the existing messages.

As you might expect, the chat bubbles are styled <div>s inside a container <div> that has overflow-y: auto and a set height.

Currently what I'm doing is noting the top message <div>, loading the older messages above it, and then repositioning the view to try and put the user back to almost where they left off, but it's tricky, especially when the chat bubbles contain dynamically loading elements (embedded images, etc). (I do not know the height of these bubbles before the browser renders them.)

In an ideal world, I would like to find a way to insert the messages above, without causing the scrolled position to move at all, so that the user doesn't lose track of where they are in the stream of messages.

I've never heard of such a thing, but it would be cool if there was a way to tell the div to stretch itself in the upward direction, rather than causing it to push the existing messages down further.

Any advice would be appreciated.

Here is a JSFiddle that shows the basic problem. It simulates my chat interface, by loading 30 chat messages when the page loads, and then puts you at the bottom of the message list. As you scroll to the top, another 30 are loaded, but you'll see that this is jarring to the UX because you instantly lose your place.

UPDATE: Thanks to Richard's answer, with a technique based on requestAnimationFrame to re-align the viewport. (For my use of it, I utilized the scrollTop of my scrollable div, rather than window.scrollTo as shown in his example.)

Share Improve this question edited Mar 19, 2020 at 0:27 Mason G. Zhwiti asked Mar 14, 2020 at 0:36 Mason G. ZhwitiMason G. Zhwiti 6,54012 gold badges64 silver badges99 bronze badges 2
  • Could you provide a minimal runnable snippet of your currently working code? – Richard Commented Mar 14, 2020 at 3:37
  • I guess you just need to take note of scroll position, add the loaded messages and scroll to new loaded messages height + old scroll positon. about images loading: you could send the height/width before loading the image, that way you can size the box beforehand. – ariel Commented Mar 14, 2020 at 3:45
Add a ment  | 

1 Answer 1

Reset to default 11

Fortunately, there is. The CSS property is called overflow-anchor. When you set the overflow-anchor property of an element to auto, it will turn on scroll anchoring; which is an attempt of the browser to minimize the problem of content jumping (such as a large image loaded above your scroll position causing your content to scroll further down); and set said element as the potential anchor when adjusting scroll position.

That being said, overflow-anchor is automatically set to auto and you needn't set it, so I'm not sure how setting it manually helped you. From MDN:

Scroll anchoring behavior is enabled by default in any browser that supports it. Therefore, changing the value of this property is typically only required if you are experiencing problems with scroll anchoring in a document or part of a document and need to turn the behavior off.

Here's a simple demonstration. At first, the window is already scrolled by 200px. Every two seconds, a new div is inserted on top of the container. Note that the visible area of the window doesn't change even when a new div is inserted on top the container.

const container = document.querySelector('#container')

window.scrollBy(0, 200)

let counter = 9
setInterval(() => {
  let newDiv = document.createElement('div')
  newDiv.classList.add('messages')
  newDiv.innerText = counter++;
  container.prepend(newDiv)
}, 2000)
* {
  box-sizing: border-box;
  font-family: Helvetica;
}

html,
body {
  margin: 0;
}

#container {
  width: 100%;
  height: 100%;
  padding: 0 20px;
  scroll-behavior: smooth;
  overflow-anchor: auto;
}

.messages {
  width: 100%;
  padding: 20px;
  text-align: center;
  background: #121212;
  color: white;
  border-radius: 5px;
  margin: 20px 0px;
}
<div id="container">
  <div class="messages">8</div>
  <div class="messages">7</div>
  <div class="messages">6</div>
  <div class="messages">5</div>
  <div class="messages">4</div>
  <div class="messages">3</div>
  <div class="messages">2</div>
  <div class="messages">1</div>
</div>


Update

As setting overflow-anchor manually did help you with adjusting scroll position upon height increase above the viewport, but Safari does not support overflow-anchor, here's a simple alternative mechanism (using requestAnimationFrame) to adjust the viewport so that it always stays in its current position. This code below also adjusts the scroll position when you are at the top of the container and older messages show up (an issue you mentioned).

Note that I've set the overflow-anchor to none, thus disabling the scroll-anchoring browsers usually enable automatically, to show that this code works (I'm running it in Firefox). Also, the code is adjusted such that it does not scroll down when new messages are appended to the container. Do consider the scenario when the user is reading the last message and a new message gets sent; you might want to scroll down in that case. Furthermore, I am calling getBoundingClientRect() every frame. If not used properly, this can cause layout thrashing (also read here for the list of JS properties/methods that can cause this).

One last note: I actually wanted to use ResizeObserver, but as Safari does not support that API, I used rAQ instead to observe container's height changes. There are already ResizeObserver polyfills, which I have not tried out yet, but may be more efficient than using rAQ to observe height changes. Here's the page that contains a lot of links to ResizeObserver polyfills.

const container = document.querySelector('#container')
let counter = 9
setInterval(() => {
  let newDiv = document.createElement('div')
  newDiv.classList.add('messages')
  newDiv.innerText = counter++;
  container.prepend(newDiv)
}, 2000)
window.scrollBy(0, 200)

// Mimicking scroll anchoring behaviour from browser
let previousLastChild = container.lastElementChild
let previousBoundingRect = container.getBoundingClientRect()
function scrollAdjustment() {
  let boundingRect = container.getBoundingClientRect()
  let isHeightIncreased = boundingRect.height !== previousBoundingRect.height
  let isAppend = container.lastElementChild !== previousLastChild // Is height increase caused by new messages being appended?
  if (isHeightIncreased && !isAppend) { // If new messages are appended, don't scroll down as people are reading the upper messages
    let newScrollYPosition = window.scrollY + boundingRect.height - previousBoundingRect.height
    
    previousBoundingRect = boundingRect
    window.scrollTo(0, newScrollYPosition)
  }
  requestAnimationFrame(scrollAdjustment)
}

requestAnimationFrame(scrollAdjustment)
* {
  box-sizing: border-box;
  font-family: Helvetica;
}

html,
body {
  margin: 0;
}

#container {
  width: 100%;
  height: 100%;
  padding: 0 20px;
  scroll-behavior: smooth;
  overflow-anchor: none; /* Note I've set it to none */
}

.messages {
  width: 100%;
  padding: 20px;
  text-align: center;
  background: #121212;
  color: white;
  border-radius: 5px;
  margin: 20px 0px;
}
<div id="container">
  <div class="messages">8</div>
  <div class="messages">7</div>
  <div class="messages">6</div>
  <div class="messages">5</div>
  <div class="messages">4</div>
  <div class="messages">3</div>
  <div class="messages">2</div>
  <div class="messages">1</div>
</div>

本文标签: javascriptInsert elements on top of something without changing visible scrolled contentStack Overflow