Mempertahankan posisi gulir hanya berfungsi bila tidak di dekat bagian bawah div pesan

10

Saya mencoba meniru aplikasi obrolan seluler lainnya di mana saat Anda memilih send-messagekotak teks dan membuka papan ketik virtual, pesan paling bawah masih terlihat. Sepertinya tidak ada cara untuk melakukan ini dengan CSS secara luar biasa, jadi JavaScript resize(satu-satunya cara untuk mengetahui kapan keyboard dibuka dan ditutup ternyata) acara dan gulir manual untuk menyelamatkan.

Seseorang memberikan solusi ini dan saya menemukan solusi ini , yang sepertinya berfungsi baik.

Kecuali dalam satu kasus. Untuk beberapa alasan, jika Anda berada dalam MOBILE_KEYBOARD_HEIGHT(250 piksel dalam kasus saya) piksel di bagian bawah div pesan, ketika Anda menutup keyboard ponsel, sesuatu yang aneh terjadi. Dengan solusi sebelumnya, itu bergulir ke bawah. Dan dengan solusi terakhir, ia akan menggulir ke atas MOBILE_KEYBOARD_HEIGHTpiksel dari bawah.

Jika Anda digulir di atas ketinggian ini, kedua solusi yang disediakan di atas berfungsi dengan sempurna. Hanya ketika Anda berada di dekat bagian bawah mereka memiliki masalah kecil ini.

Saya pikir mungkin itu hanya program saya yang menyebabkan ini dengan beberapa kode nyasar yang aneh, tapi tidak, saya bahkan mereproduksi biola dan memiliki masalah yang tepat ini. Maafkan saya karena membuat ini sangat sulit untuk di-debug, tetapi jika Anda membuka https://jsfiddle.net/t596hy8d/6/show (suffix show menyediakan mode layar penuh) pada ponsel Anda, Anda seharusnya dapat melihat perilaku yang sama.

Perilaku itu adalah, jika Anda menggulir ke atas cukup, membuka dan menutup keyboard mempertahankan posisi. Namun, jika Anda menutup keyboard dalam MOBILE_KEYBOARD_HEIGHTpiksel bagian bawah, Anda akan menemukan keyboard itu menggulir ke bagian bawah.

Apa yang menyebabkan ini?

Reproduksi kode di sini:

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>

Ryan Peschel
sumber
Saya akan mengganti event handler dengan IntersectionObserver dan ResizeObserver. Mereka memiliki overhead CPU yang jauh lebih rendah daripada event handler. Jika Anda menargetkan browser yang lebih lama, keduanya memiliki polyfill.
bigless
Sudahkah Anda mencoba ini di Firefox untuk perangkat seluler? Tampaknya tidak memiliki masalah ini. Namun, mencoba ini di Chrome memang menyebabkan masalah yang Anda sebutkan.
Richard
Yah, itu harus bekerja di Chrome. Itu bagus Firefox tidak memiliki masalah.
Ryan Peschel
Buruk saya karena tidak menyampaikan poin saya dengan benar. Jika satu browser memiliki masalah dan lainnya tidak, ini, IMO, mungkin berarti bahwa Anda mungkin perlu memiliki implementasi yang sedikit berbeda untuk browser yang berbeda.
Richard
1
@ Pesel Baiklah. Saya melihat. Terima kasih atas pengingatnya, saya akan mempertimbangkannya saat berikutnya saya meminta seseorang untuk melihat kembali sebuah jawaban.
Richard

Jawaban:

3

Saya akhirnya menemukan solusi yang benar - benar berfungsi. Meskipun mungkin tidak ideal, itu sebenarnya berfungsi dalam semua kasus. Ini kodenya:

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

Beberapa pencerahan yang saya jalani:

  1. Saat menutup keyboard virtual, scrollacara terjadi secara instan sebelum resizeacara. Ini sepertinya hanya terjadi ketika menutup keyboard, bukan membukanya. Ini adalah alasan Anda tidak dapat menggunakan scrollacara untuk mengatur pxFromBottom, karena jika Anda berada di dekat bagian bawah itu akan mengatur sendiri ke 0 di scrollacara tepat sebelum resizeacara, mengacaukan perhitungan.

  2. Alasan lain mengapa semua solusi mengalami kesulitan di dekat bagian bawah div pesan agak sulit dipahami. Sebagai contoh, dalam solusi ukuran saya, saya hanya menambah atau mengurangi 250 (tinggi keyboard ponsel) scrollTopketika membuka atau menutup keyboard virtual. Ini berfungsi dengan baik kecuali di dekat bagian bawah. Mengapa? Karena katakanlah Anda 50 piksel dari bawah dan tutup keyboard. Ini akan mengurangi 250 dari scrollTop(ketinggian keyboard), tetapi seharusnya hanya mengurangi 50! Jadi itu akan selalu mengatur ulang ke posisi tetap yang salah saat menutup keyboard di dekat bagian bawah.

  3. Saya juga percaya Anda tidak dapat menggunakan onFocusdan onBluracara untuk solusi ini, karena itu hanya terjadi ketika awalnya memilih kotak teks untuk membuka keyboard. Anda dapat dengan sempurna membuka dan menutup keyboard ponsel tanpa mengaktifkan acara ini, dan karenanya, mereka tidak dapat digunakan di sini.

Saya percaya poin di atas penting untuk mengembangkan solusi, karena mereka tidak jelas pada awalnya, tetapi mencegah solusi yang kuat dari pengembangan.

Saya tidak suka solusi ini (intervalnya sedikit tidak efisien dan rentan terhadap kondisi balapan), tetapi saya tidak dapat menemukan yang lebih baik yang selalu berfungsi.

Ryan Peschel
sumber
1

Saya pikir apa yang Anda inginkan overflow-anchor

Dukungan meningkat, tetapi tidak total, namun https://caniuse.com/#feat=css-overflow-anchor

Dari artikel CSS-Trik di atasnya:

Scroll Anchoring mencegah pengalaman "lompatan" dengan mengunci posisi pengguna pada halaman saat perubahan terjadi di DOM di atas lokasi saat ini. Ini memungkinkan pengguna untuk tetap berlabuh di mana mereka berada di halaman bahkan ketika elemen-elemen baru dimuat ke DOM.

Properti overflow-anchor memungkinkan kita untuk memilih keluar dari fitur Penanda Gulir jika itu lebih disukai untuk memungkinkan konten yang akan mengalir kembali ketika elemen dimuat.

Ini adalah versi yang sedikit dimodifikasi dari salah satu contohnya:

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>

Buka ini di ponsel: https://cdpn.io/chasebank/debug/PowxdOR

Apa yang dilakukan pada dasarnya menonaktifkan penahan default elemen pesan baru #scroller * { overflow-anchor: none }

Dan alih-alih menempelkan elemen kosong #anchor { overflow-anchor: auto }yang akan selalu datang setelah pesan-pesan baru, karena pesan-pesan baru dimasukkan sebelum itu.

Harus ada gulir untuk melihat perubahan penahan, yang menurut saya umumnya baik UX. Namun demikian, posisi gulir saat ini harus dipertahankan ketika keyboard terbuka.

Mengejar
sumber
0

Solusi saya sama dengan solusi yang Anda ajukan dengan tambahan pemeriksaan bersyarat. Inilah uraian solusi saya:

  • Merekam posisi scroll terakhir scrollTopdan terakhir clientHeightdari .messagesuntuk oldScrollTopdan oldHeightmasing-masing
  • Perbarui oldScrollTopdan oldHeightsetiap kali resizeterjadi windowdan perbarui oldScrollTopsetiap kali scrollterjadi.messages
  • Ketika windowmenyusut (ketika keyboard virtual menunjukkan), ketinggian .messagesakan secara otomatis ditarik. Perilaku yang dimaksudkan adalah untuk membuat konten paling bawah dari .messagesmasih terlihat bahkan ketika .messages'memendek. Hal ini mengharuskan kita untuk secara manual mengatur posisi gulir scrollTopdari .messages.
  • Ketika menunjukkan keyboard virtual, pembaruan scrollTopdari .messagesmemastikan bahwa bagian paling bawah dari .messagessebelum pencabutan tingginya terjadi adalah masih terlihat
  • Ketika kulit keyboard virtual, pembaruan scrollTopdari .messagesmemastikan bahwa bagian paling bawah dari .messagessisa-sisa bagian paling bawah dari .messagessetelah ekspansi tinggi (kecuali ekspansi tidak bisa terjadi ke atas, ini terjadi ketika Anda hampir di bagian atas .messages)

Apa yang menyebabkan masalah?

Pemikiran logis saya (mungkin cacat) adalah: resizeterjadi, .messages'perubahan tinggi, pembaruan .messages scrollTopterjadi di dalam resizepengendali acara kami . Namun, setelah .messagesekspansi tinggi, sebuah scrollperistiwa anehnya terjadi sebelum resize! Dan yang lebih aneh lagi, scrollkejadian itu hanya terjadi ketika kita menyembunyikan keyboard ketika kita telah menggulir di atas nilai maksimum scrollTopketika .messagestidak ditarik. Dalam kasus saya, ini berarti bahwa ketika saya gulir di bawah 270.334px(maksimum scrollTopsebelum .messagesditarik) dan menyembunyikan keyboard, itu aneh scrollsebelum resizeacara terjadi dan menggulir Anda .messageske persis 270.334px. Ini jelas mengacaukan solusi kami di atas.

Untungnya, kita bisa mengatasi ini. Deduksi pribadi saya tentang mengapa ini scrollsebelum resizeperistiwa terjadi adalah karena .messagestidak dapat mempertahankan scrollTopposisinya di atas 270.334pxketika mengembang tinggi (ini sebabnya saya menyebutkan bahwa pemikiran logis awal saya cacat; hanya karena tidak ada cara untuk .messagesmempertahankan scrollTopposisinya di atas maksimum nilai) . Oleh karena itu, ia segera menetapkan scrollTopnilai maksimum yang dapat diberikannya (yaitu, tidak mengejutkan, 270.334px).

Apa yang bisa kita lakukan?

Karena kita hanya memperbarui oldHeightpada pengubahan ukuran, kita dapat memeriksa apakah scroll paksa ini (atau lebih tepatnya, resize) terjadi dan jika tidak, tidak memperbarui oldScrollTop(karena kita sudah menangani itu di resize!) Kita hanya perlu membandingkan oldHeightdan ketinggian saat ini di scrolluntuk melihat apakah pengguliran paksa ini terjadi. Ini berfungsi karena kondisi oldHeighttidak sama dengan ketinggian saat scrollini hanya akan benar ketika resizeterjadi (yang kebetulan ketika pengguliran paksa terjadi).

Berikut kode (dalam JSFiddle) di bawah:

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>

Diuji pada Firefox dan Chrome untuk seluler dan berfungsi untuk kedua browser.

Richard
sumber