Duy trì vị trí cuộn chỉ hoạt động khi không ở gần cuối thư div


10

Tôi đang cố gắng bắt chước các ứng dụng trò chuyện trên thiết bị di động khác khi bạn chọn send-messagehộp văn bản và nó mở bàn phím ảo, tin nhắn dưới cùng vẫn còn trong tầm nhìn. Dường như không có cách nào để làm điều này với CSS một cách đáng kinh ngạc, vì vậy JavaScript resize(chỉ có cách tìm ra khi bàn phím được mở và đóng rõ ràng) các sự kiện và cuộn thủ công để giải cứu.

Ai đó đã cung cấp giải pháp này và tôi đã tìm ra giải pháp này , cả hai đều có vẻ hiệu quả.

Ngoại trừ trong một trường hợp. Vì một số lý do, nếu bạn ở trong MOBILE_KEYBOARD_HEIGHT(250 pixel trong trường hợp của tôi) pixel ở dưới cùng của div tin nhắn, khi bạn đóng bàn phím di động, điều gì đó sẽ xảy ra. Với giải pháp trước đây, nó cuộn xuống phía dưới. Và với giải pháp sau, nó thay vào đó cuộn lên các MOBILE_KEYBOARD_HEIGHTpixel từ phía dưới.

Nếu bạn được cuộn trên độ cao này, cả hai giải pháp được cung cấp ở trên đều hoạt động hoàn hảo. Chỉ khi bạn ở gần đáy thì họ mới gặp phải vấn đề nhỏ này.

Tôi nghĩ có lẽ đó chỉ là chương trình của tôi gây ra điều này với một số mã đi lạc kỳ lạ, nhưng không, tôi thậm chí còn sao chép một câu đố và nó có vấn đề chính xác này. Tôi xin lỗi vì làm cho việc này rất khó để gỡ lỗi, nhưng nếu bạn truy cập https://jsfiddle.net/t596hy8d/6/show (hậu tố hiển thị cung cấp chế độ toàn màn hình) trên điện thoại của bạn, bạn sẽ có thể thấy cùng hành vi.

Hành vi đó là, nếu bạn cuộn lên đủ, mở và đóng bàn phím sẽ duy trì vị trí. Tuy nhiên, nếu bạn đóng bàn phím trong các MOBILE_KEYBOARD_HEIGHTpixel ở phía dưới, bạn sẽ thấy rằng nó sẽ cuộn xuống phía dưới thay thế.

Điều gì gây ra điều này?

Tái tạo mã ở đây:

window.onload = function(e){ 
  document.querySelector(".messages").scrollTop = 10000;
  
  bottomScroller(document.querySelector(".messages"));
}
  

function bottomScroller(scroller) {
  let scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;

  scroller.addEventListener('scroll', () => { 
  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });   

  window.addEventListener('resize', () => { 
  scroller.scrollTop = scroller.scrollHeight - scrollBottom - scroller.clientHeight;

  scrollBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
  <div class="message">hello 1</div>
  <div class="message">hello 2</div>
  <div class="message">hello 3</div>
  <div class="message">hello 4</div>
  <div class="message">hello 5</div>
  <div class="message">hello 6 </div>
  <div class="message">hello 7</div>
  <div class="message">hello 8</div>
  <div class="message">hello 9</div>
  <div class="message">hello 10</div>
  <div class="message">hello 11</div>
  <div class="message">hello 12</div>
  <div class="message">hello 13</div>
  <div class="message">hello 14</div>
  <div class="message">hello 15</div>
  <div class="message">hello 16</div>
  <div class="message">hello 17</div>
  <div class="message">hello 18</div>
  <div class="message">hello 19</div>
  <div class="message">hello 20</div>
  <div class="message">hello 21</div>
  <div class="message">hello 22</div>
  <div class="message">hello 23</div>
  <div class="message">hello 24</div>
  <div class="message">hello 25</div>
  <div class="message">hello 26</div>
  <div class="message">hello 27</div>
  <div class="message">hello 28</div>
  <div class="message">hello 29</div>
  <div class="message">hello 30</div>
  <div class="message">hello 31</div>
  <div class="message">hello 32</div>
  <div class="message">hello 33</div>
  <div class="message">hello 34</div>
  <div class="message">hello 35</div>
  <div class="message">hello 36</div>
  <div class="message">hello 37</div>
  <div class="message">hello 38</div>
  <div class="message">hello 39</div>
  </div>
  <div class="send-message">
	<input />
  </div>
</div>


Tôi sẽ thay thế trình xử lý sự kiện bằng IntersectionObserver và ResizeObserver. Chúng có chi phí CPU thấp hơn nhiều so với xử lý sự kiện. Nếu bạn đang nhắm mục tiêu các trình duyệt cũ hơn, cả hai đều có polyfill.
bigless

Bạn đã thử điều này trên Firefox cho thiết bị di động chưa? Nó dường như không có vấn đề này. Tuy nhiên, việc thử này trên Chrome không gây ra sự cố mà bạn đã đề cập.
Richard

Chà, dù sao nó cũng phải hoạt động trên Chrome. Mặc dù vậy, Firefox không có vấn đề gì.
Ryan Peschel

Xấu của tôi vì không truyền đạt quan điểm của tôi đúng. Nếu một trình duyệt có vấn đề và một trình duyệt khác thì không, IMO, có thể có nghĩa là bạn có thể cần phải thực hiện một chút khác nhau cho các trình duyệt khác nhau.
Richard

1
@halfer Được rồi. Tôi hiểu rồi. Cảm ơn bạn đã nhắc nhở, tôi sẽ tính đến điều đó vào lần tới khi tôi yêu cầu ai đó xem lại câu trả lời.
Richard

Câu trả lời:


3

Cuối cùng tôi đã tìm thấy một giải pháp thực sự hoạt động. Mặc dù nó có thể không lý tưởng, nhưng nó thực sự hoạt động trong mọi trường hợp. Đây là mã:

bottomScroller(document.querySelector(".messages"));

bottomScroller = scroller => {
  let pxFromBottom = 0;

  let calcPxFromBottom = () => pxFromBottom = scroller.scrollHeight - (scroller.scrollTop + scroller.clientHeight);

  setInterval(calcPxFromBottom, 500);

  window.addEventListener('resize', () => { 
    scroller.scrollTop = scroller.scrollHeight - pxFromBottom - scroller.clientHeight;
  });
}

Một số epiphère tôi đã có trên đường đi:

  1. Khi đóng bàn phím ảo, một scrollsự kiện sẽ xảy ra ngay trước resizesự kiện. Điều này dường như chỉ xảy ra khi đóng bàn phím, không mở nó. Đây là lý do bạn không thể sử dụng scrollsự kiện để đặt pxFromBottom, vì nếu bạn ở gần đáy, nó sẽ tự đặt thành 0 trong scrollsự kiện ngay trước resizesự kiện, làm rối tung phép tính.

  2. Một lý do khác khiến tất cả các giải pháp gặp khó khăn ở gần cuối thư div là một chút khó hiểu. Ví dụ, trong giải pháp thay đổi kích thước của tôi, tôi chỉ cần thêm hoặc trừ 250 (chiều cao bàn phím di động) scrollTopkhi mở hoặc đóng bàn phím ảo. Điều này hoạt động hoàn hảo ngoại trừ gần dưới cùng. Tại sao? Bởi vì giả sử bạn cách 50 pixel từ dưới lên và đóng bàn phím. Nó sẽ trừ 250 từ scrollTop(chiều cao bàn phím), nhưng chỉ nên trừ 50! Vì vậy, nó sẽ luôn đặt lại vị trí cố định sai khi đóng bàn phím gần phía dưới.

  3. Tôi cũng tin rằng bạn không thể sử dụng onFocusonBlurcác sự kiện cho giải pháp này, vì những điều đó chỉ xảy ra khi ban đầu chọn hộp văn bản để mở bàn phím. Bạn hoàn toàn có thể mở và đóng bàn phím di động mà không cần kích hoạt các sự kiện này và do đó, chúng không thể được sử dụng ở đây.

Tôi tin rằng những điểm trên rất quan trọng để phát triển một giải pháp, vì ban đầu chúng không rõ ràng, nhưng ngăn cản một giải pháp mạnh mẽ phát triển.

Tôi không thích giải pháp này (khoảng thời gian hơi kém hiệu quả và dễ bị điều kiện chủng tộc), nhưng tôi không thể tìm thấy bất cứ điều gì tốt hơn luôn luôn hoạt động.


1

Tôi nghĩ những gì bạn muốn là overflow-anchor

Hỗ trợ đang tăng, nhưng không phải là toàn bộ, nhưng https://caniuse.com/#feat=css-overflow-anchor

Từ một bài viết Thủ thuật CSS về nó:

Cuộn neo ngăn chặn trải nghiệm "nhảy" bằng cách khóa vị trí của người dùng trên trang trong khi các thay đổi đang diễn ra trong DOM phía trên vị trí hiện tại. Điều này cho phép người dùng ở lại vị trí neo trên trang ngay cả khi các phần tử mới được tải vào DOM.

Thuộc tính neo tràn cho phép chúng ta từ chối tính năng Cuộn neo trong trường hợp được ưu tiên cho phép nội dung được truyền lại khi các phần tử được tải.

Đây là một phiên bản sửa đổi một chút của một trong những ví dụ của họ:

let scroller = document.querySelector('#scroller');
let anchor = document.querySelector('#anchor');

// https://ajaydsouza.com/42-phrases-a-lexophile-would-love/
let messages = [
  'I wondered why the baseball was getting bigger. Then it hit me.',
  'Police were called to a day care, where a three-year-old was resisting a rest.',
  'Did you hear about the guy whose whole left side was cut off? He’s all right now.',
  'The roundest knight at King Arthur’s round table was Sir Cumference.',
  'To write with a broken pencil is pointless.',
  'When fish are in schools they sometimes take debate.',
  'The short fortune teller who escaped from prison was a small medium at large.',
  'A thief who stole a calendar… got twelve months.',
  'A thief fell and broke his leg in wet cement. He became a hardened criminal.',
  'Thieves who steal corn from a garden could be charged with stalking.',
  'When the smog lifts in Los Angeles , U. C. L. A.',
  'The math professor went crazy with the blackboard. He did a number on it.',
  'The professor discovered that his theory of earthquakes was on shaky ground.',
  'The dead batteries were given out free of charge.',
  'If you take a laptop computer for a run you could jog your memory.',
  'A dentist and a manicurist fought tooth and nail.',
  'A bicycle can’t stand alone; it is two tired.',
  'A will is a dead giveaway.',
  'Time flies like an arrow; fruit flies like a banana.',
  'A backward poet writes inverse.',
  'In a democracy it’s your vote that counts; in feudalism, it’s your Count that votes.',
  'A chicken crossing the road: poultry in motion.',
  'If you don’t pay your exorcist you can get repossessed.',
  'With her marriage she got a new name and a dress.',
  'Show me a piano falling down a mine shaft and I’ll show you A-flat miner.',
  'When a clock is hungry it goes back four seconds.',
  'The guy who fell onto an upholstery machine was fully recovered.',
  'A grenade fell onto a kitchen floor in France and resulted in Linoleum Blownapart.',
  'You are stuck with your debt if you can’t budge it.',
  'Local Area Network in Australia : The LAN down under.',
  'He broke into song because he couldn’t find the key.',
  'A calendar’s days are numbered.',
];

function randomMessage() {
  return messages[(Math.random() * messages.length) | 0];
}

function appendChild() {
  let msg = document.createElement('div');
  msg.className = 'message';
  msg.innerText = randomMessage();
  scroller.insertBefore(msg, anchor);
}
setInterval(appendChild, 1000);
html {
  height: 100%;
  display: flex;
}

body {
  min-height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  padding: 0;
}

#scroller {
  flex: 2;
}

#scroller * {
  overflow-anchor: none;
}

.new-message {
  position: sticky;
  bottom: 0;
  background-color: blue;
  padding: .2rem;
}

#anchor {
  overflow-anchor: auto;
  height: 1px;
}

body {
  background-color: #7FDBFF;
}

.message {
  padding: 0.5em;
  border-radius: 1em;
  margin: 0.5em;
  background-color: white;
}
<div id="scroller">
  <div id="anchor"></div>
</div>

<div class="new-message">
  <input type="text" placeholder="New Message">
</div>

Mở cái này trên di động: https://cdpn.io/chasebank/debug/PowxdOR

Những gì đang làm về cơ bản là vô hiệu hóa bất kỳ neo mặc định nào của các thành phần thông báo mới, với #scroller * { overflow-anchor: none }

Và thay vào đó, neo một phần tử trống #anchor { overflow-anchor: auto }sẽ luôn xuất hiện sau các tin nhắn mới đó, vì các tin nhắn mới đang được chèn trước nó.

Phải có một cuộn để nhận thấy một sự thay đổi trong neo, mà tôi nghĩ nói chung là UX tốt. Nhưng dù bằng cách nào, vị trí cuộn hiện tại nên được duy trì khi bàn phím mở.


0

Giải pháp của tôi giống như giải pháp đề xuất của bạn với việc bổ sung kiểm tra có điều kiện. Dưới đây là mô tả về giải pháp của tôi:

  • Ghi lại vị trí di chuyển cuối cùng scrollTopvà cuối cùng clientHeightcủa .messagesđể oldScrollTopoldHeighttương ứng
  • Cập nhật oldScrollTopoldHeightmỗi khi resizexảy ra windowvà cập nhật oldScrollTopmỗi khi scrollxảy ra.messages
  • Khi windowđược thu nhỏ (khi bàn phím ảo hiển thị), chiều cao của .messagessẽ tự động rút lại. Hành vi dự định là làm cho nội dung cuối cùng .messagesvẫn hiển thị ngay cả khi .messages'chiều cao rút lại. Điều này đòi hỏi chúng ta phải tự điều chỉnh vị trí cuộn scrollTopcủa .messages.
  • Khi các chương trình bàn phím ảo, cập nhật scrollTopcủa .messagesđể đảm bảo rằng phần dưới cùng của .messagestrước chiều cao của nó rút lại xảy ra là vẫn còn nhìn thấy
  • Khi da bàn phím ảo, cập nhật scrollTopcủa .messagesđể đảm bảo rằng phần dưới cùng của .messagesphần còn lại phần dưới cùng của .messagessau khi mở rộng chiều cao (trừ khi việc mở rộng không thể xảy ra trở lên, điều này sẽ xảy ra khi bạn gần như ở đầu .messages)

Điều gì gây ra vấn đề?

Suy nghĩ logic (ban đầu có thể thiếu sót) của tôi là: resizexảy ra, .messages'thay đổi chiều cao, cập nhật .messages scrollTopxảy ra bên trong bộ resizexử lý sự kiện của chúng tôi . Tuy nhiên, khi .messages'mở rộng chiều cao, một scrollsự kiện tò mò xảy ra trước khi a resize! Và thậm chí tò mò hơn, scrollsự kiện chỉ xảy ra khi chúng ta ẩn bàn phím khi chúng ta đã cuộn trên scrollTopgiá trị tối đa khi .messageskhông được rút lại. Trong trường hợp của tôi, điều này có nghĩa là khi tôi cuộn bên dưới 270.334px(mức tối đa scrollToptrước .messagesđược rút lại) và ẩn bàn phím, điều kỳ lạ scrollđó trước khi resizesự kiện xảy ra và cuộn .messageschính xác của bạn 270.334px. Điều này rõ ràng làm rối tung giải pháp của chúng tôi ở trên.

May mắn thay, chúng ta có thể làm việc xung quanh này. Suy luận cá nhân của tôi về lý do tại sao điều này scrolltrước khi resizesự kiện xảy ra là vì .messageskhông thể duy trì scrollTopvị trí của nó ở trên 270.334pxkhi nó mở rộng về chiều cao (đây là lý do tại sao tôi đã đề cập rằng suy nghĩ logic ban đầu của tôi là thiếu sót; đơn giản là vì không có cách nào .messagesđể duy trì scrollTopvị trí của nó trên mức tối đa giá trị) . Do đó, nó ngay lập tức đặt scrollTopgiá trị tối đa mà nó có thể cung cấp (không có gì đáng ngạc nhiên 270.334px).

Chúng ta có thể làm gì?

Vì chúng tôi chỉ cập nhật oldHeightkhi thay đổi kích thước, chúng tôi có thể kiểm tra xem cuộn bắt buộc này (hay chính xác hơn là resize) xảy ra và nếu có, đừng cập nhật oldScrollTop(vì chúng tôi đã xử lý trong đó resize!) Chúng tôi chỉ cần so sánh oldHeightvà chiều cao hiện tại trên scrollđể xem nếu cuộn này buộc phải xảy ra. Điều này hoạt động vì điều kiện oldHeightkhông bằng chiều cao hiện tại scrollsẽ chỉ đúng khi resizexảy ra (điều này là trùng hợp khi điều đó bắt buộc di chuyển).

Đây là mã (trong JSFiddle) bên dưới:

window.onload = function(e) {
  let messages = document.querySelector('.messages')
  messages.scrollTop = messages.scrollHeight - messages.clientHeight
  bottomScroller(messages);
}


function bottomScroller(scroller) {
  let oldScrollTop = scroller.scrollTop
  let oldHeight = scroller.clientHeight

  scroller.addEventListener('scroll', e => {
    console.log(`Scroll detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${scroller.scrollTop}`)
    if (oldHeight === scroller.clientHeight)
      oldScrollTop = scroller.scrollTop
  });

  window.addEventListener('resize', e => {
    let newScrollTop = oldScrollTop + oldHeight - scroller.clientHeight

    console.log(`Resize detected:
      old scroll top = ${oldScrollTop},
      old height = ${oldHeight},
      new height = ${scroller.clientHeight},
      new scroll top = ${newScrollTop}`)
    scroller.scrollTop = newScrollTop
    oldScrollTop = newScrollTop
    oldHeight = scroller.clientHeight
  });
}
.container {
  width: 400px;
  height: 87vh;
  border: 1px solid #333;
  display: flex;
  flex-direction: column;
}

.messages {
  overflow-y: auto;
  height: 100%;
}

.send-message {
  width: 100%;
  display: flex;
  flex-direction: column;
}
<div class="container">
  <div class="messages">
    <div class="message">hello 1</div>
    <div class="message">hello 2</div>
    <div class="message">hello 3</div>
    <div class="message">hello 4</div>
    <div class="message">hello 5</div>
    <div class="message">hello 6 </div>
    <div class="message">hello 7</div>
    <div class="message">hello 8</div>
    <div class="message">hello 9</div>
    <div class="message">hello 10</div>
    <div class="message">hello 11</div>
    <div class="message">hello 12</div>
    <div class="message">hello 13</div>
    <div class="message">hello 14</div>
    <div class="message">hello 15</div>
    <div class="message">hello 16</div>
    <div class="message">hello 17</div>
    <div class="message">hello 18</div>
    <div class="message">hello 19</div>
    <div class="message">hello 20</div>
    <div class="message">hello 21</div>
    <div class="message">hello 22</div>
    <div class="message">hello 23</div>
    <div class="message">hello 24</div>
    <div class="message">hello 25</div>
    <div class="message">hello 26</div>
    <div class="message">hello 27</div>
    <div class="message">hello 28</div>
    <div class="message">hello 29</div>
    <div class="message">hello 30</div>
    <div class="message">hello 31</div>
    <div class="message">hello 32</div>
    <div class="message">hello 33</div>
    <div class="message">hello 34</div>
    <div class="message">hello 35</div>
    <div class="message">hello 36</div>
    <div class="message">hello 37</div>
    <div class="message">hello 38</div>
    <div class="message">hello 39</div>
  </div>
  <div class="send-message">
    <input />
  </div>
</div>

Đã thử nghiệm trên Firefox và Chrome cho thiết bị di động và nó hoạt động cho cả hai trình duyệt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.