Utrzymanie pozycji przewijania działa tylko wtedy, gdy nie znajduje się w dolnej części wiadomości div

10

Próbuję naśladować inne mobilne aplikacje do czatowania, w których po wybraniu opcji send-message tekstowego i otwarciu wirtualnej klawiatury nadal wyświetla się najniższa wiadomość. Wydaje się, że nie ma sposobu, aby zrobić to z CSS w zadziwiający sposób, więc JavaScript resize(jedyny sposób, aby dowiedzieć się, kiedy klawiatura jest najwyraźniej otwarta i zamknięta) zdarzenia i ręczne przewijanie na ratunek.

Ktoś dostarczył to rozwiązanie i się dowiedziałem to rozwiązanie , które wydaje się działać.

Z wyjątkiem jednego przypadku. Z jakiegoś powodu, jeśli jesteś w środkuMOBILE_KEYBOARD_HEIGHT (w moim przypadku 250 pikseli) pikseli od dolnej części wiadomości div, po zamknięciu klawiatury mobilnej dzieje się coś dziwnego. W pierwszym rozwiązaniu przewija się do dołu. W tym drugim rozwiązaniu zamiast tego przewija MOBILE_KEYBOARD_HEIGHTpiksele w górę od dołu.

Jeśli przewiniesz powyżej tej wysokości, oba powyższe rozwiązania działają bezbłędnie. Tylko wtedy, gdy jesteś blisko dna, mają ten drobny problem.

Pomyślałem, że może to tylko mój program powodował ten dziwny zbłąkany kod, ale nie, nawet odtworzyłem skrzypce i ma dokładnie ten problem. Przepraszamy za utrudnienie debugowania, ale jeśli przejdziesz do https://jsfiddle.net/t596hy8d/6/show (sufiks pokazu zapewnia tryb pełnoekranowy) na telefonie, powinieneś zobaczyć takie samo zachowanie.

Takie zachowanie polega na tym, że jeśli przewiniesz wystarczająco, otwarcie i zamknięcie klawiatury utrzymuje pozycję. Jeśli jednak zamkniesz klawiaturę w odległości kilku MOBILE_KEYBOARD_HEIGHTpikseli od dołu, przekonasz się, że zamiast tego przewija się ona w dół.

Co to powoduje?

Reprodukcja kodu tutaj:

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
źródło
Zastąpiłbym programy obsługi zdarzeń IntersectionObserver i ResizeObserver. Mają znacznie niższe obciążenie procesora niż procedury obsługi zdarzeń. Jeśli kierujesz reklamy na starsze przeglądarki, oba mają wielorakie wypełnienia.
bigless
Czy próbowałeś tego w przeglądarce Firefox dla urządzeń mobilnych? Wydaje się, że nie ma tego problemu. Jednak wypróbowanie tego w Chrome powoduje wspomniany problem.
Richard
Cóż, i tak musi działać w Chrome. To miłe, że Firefox nie ma problemu.
Ryan Peschel
Moje złe, że nie przekazałem poprawnie mojego punktu. Jeśli jedna przeglądarka ma problem, a inna nie, to IMO może oznaczać, że być może będziesz potrzebować nieco innej implementacji dla różnych przeglądarek.
Richard
1
@halfer W porządku. Widzę. Dziękuję za przypomnienie, wezmę to pod uwagę, kiedy następnym razem poproszę kogoś o ponowne sprawdzenie odpowiedzi.
Richard

Odpowiedzi:

3

W końcu znalazłem rozwiązanie, które faktycznie działa. Chociaż może nie być idealny, w rzeczywistości działa we wszystkich przypadkach. Oto kod:

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

Kilka objawień, które miałem po drodze:

  1. Po zamknięciu wirtualnej klawiatury scrollzdarzenie następuje bezpośrednio przed nim resize. Wydaje się, że dzieje się tak tylko podczas zamykania klawiatury, a nie jej otwierania. To jest powód, dla którego nie można użyć scrollzdarzenia do ustawienia pxFromBottom, ponieważ jeśli jesteś blisko dna, ustawi się na 0 w scrollzdarzeniu tuż przed resizewydarzeniem, co psuje obliczenia.

  2. Kolejny powód, dla którego wszystkie rozwiązania miały trudności w dolnej części wiadomości div, jest nieco trudny do zrozumienia. Na przykład w moim rozwiązaniu zmiany rozmiaru po prostu dodaję lub odejmuję 250 (wysokość klawiatury mobilnej) scrollToppodczas otwierania lub zamykania wirtualnej klawiatury. Działa to doskonale, z wyjątkiem dolnej krawędzi. Dlaczego? Ponieważ powiedzmy, że masz 50 pikseli od dołu i zamknij klawiaturę. Odejmie 250 od scrollTop(wysokość klawiatury), ale powinno odjąć tylko 50! Dlatego zawsze zamyka się w złym ustalonym położeniu podczas zamykania klawiatury w pobliżu dna.

  3. Uważam również, że nie można używać onFocusi onBlurzdarzeń dla tego rozwiązania, ponieważ występują one tylko przy początkowym zaznaczeniu pola tekstowego w celu otwarcia klawiatury. Możesz doskonale otwierać i zamykać klawiaturę mobilną bez aktywowania tych zdarzeń i dlatego nie można ich tutaj używać.

Uważam, że powyższe punkty są ważne przy opracowywaniu rozwiązania, ponieważ początkowo nie są oczywiste, ale uniemożliwiają opracowanie solidnego rozwiązania.

Nie podoba mi się to rozwiązanie (interwał jest nieco nieefektywny i podatny na warunki wyścigowe), ale nie mogę znaleźć niczego lepszego, co zawsze by działało.

Ryan Peschel
źródło
1

Myślę, że chcesz overflow-anchor

Wsparcie rośnie, ale jeszcze nie jest całkowite https://caniuse.com/#feat=css-overflow-anchor

Od artykułu CSS-Tricks na ten temat:

Przewijanie zakotwiczenia zapobiega „przeskakiwaniu” przez blokowanie pozycji użytkownika na stronie, podczas gdy zmiany zachodzą w DOM powyżej bieżącej lokalizacji. Pozwala to użytkownikowi pozostać zakotwiczonym tam, gdzie są na stronie, nawet gdy nowe elementy są ładowane do DOM.

Właściwość overflow-anchor pozwala nam zrezygnować z funkcji zakotwiczenia przewijania w przypadku, gdy preferowane jest zezwolenie na ponowne przesyłanie treści podczas ładowania elementów.

Oto nieco zmodyfikowana wersja jednego z ich przykładów:

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>

Otwórz to na telefonie komórkowym: https://cdpn.io/chasebank/debug/PowxdOR

To, co robi, polega zasadniczo na wyłączeniu domyślnego zakotwiczenia nowych elementów wiadomości za pomocą #scroller * { overflow-anchor: none }

Zamiast tego zakotwiczenie pustego elementu #anchor { overflow-anchor: auto }, który zawsze pojawi się po tych nowych wiadomościach, ponieważ nowe wiadomości są wstawiane przed nim.

Musi istnieć zwój, aby zauważyć zmianę w zakotwiczeniu, co moim zdaniem jest ogólnie dobrym UX. Ale w obu przypadkach bieżąca pozycja przewijania powinna zostać zachowana po otwarciu klawiatury.

Gonić
źródło
0

Moje rozwiązanie jest takie samo, jak proponowane przez ciebie rozwiązanie z dodatkiem kontroli warunkowej. Oto opis mojego rozwiązania:

  • Nagrać ostatnią pozycję przewijania scrollTopi ostatni clientHeightod .messagescelu oldScrollTopi oldHeightodpowiednio
  • Aktualizuj oldScrollTopi za oldHeightkażdym razem, gdy coś się resizewydarzy windowi aktualizuj za oldScrollTopkażdym razem, gdy coś się scrolldzieje.messages
  • Po windowzmniejszeniu (gdy pokazuje się wirtualna klawiatura) wysokość .messagesautomatycznie się cofnie. Zamierzonym zachowaniem jest sprawienie, aby najniższa zawartość była .messagesnadal widoczna, nawet gdy .messageswysokość się cofa. To wymaga od nas, aby ręcznie wyregulować pozycję przewijania scrollTopz .messages.
  • Podczas pokazów wirtualnej klawiatury, aktualizacja scrollTopz .messagesaby upewnić się, że spodnia część .messagesprzed jej wysokość wycofania się dzieje, jest nadal widoczne
  • Gdy wirtualne skóry klawiatury, aktualizacja scrollTopz .messagesaby upewnić się, że spodnia część .messagespozostaje najniżej część .messagespo rozwinięciu wysokość (chyba że ekspansja nie może zdarzyć się w górę, co dzieje się, gdy jesteś prawie na szczycie .messages)

Co spowodowało problem?

Moje (początkowo błędne) logiczne myślenie brzmi: resizedzieje się, .messageszmiany wysokości, aktualizacja .messages scrollTopdzieje się w naszym module resizeobsługi zdarzeń. Jednak po .messages„zwiększeniu wysokości” scrollwydarzenie ciekawie zdarza się przed resize! Co więcej, scrollzdarzenie ma miejsce tylko wtedy, gdy ukrywamy klawiaturę, gdy przewijamy powyżej maksymalnej scrollTopwartości, gdy .messagesnie jest ona cofnięta. W moim przypadku oznacza to, że kiedy przewijam poniżej 270.334px(maksimum scrollTopprzed .messagesjest cofnięte) i chowam klawiaturę, to dziwne, scrollzanim resizewydarzy się zdarzenie i przewija twoje .messagesdo dokładnie270.334px . To oczywiście psuje nasze powyższe rozwiązanie.

Na szczęście możemy to obejść. Moją osobistą dedukcją, dlaczego tak scrollsię resizestało przed wydarzeniem, jest to, że .messagesnie mogę utrzymać scrollToppozycji powyżej, 270.334pxgdy rośnie ona na wysokość (dlatego wspomniałem, że moje początkowe logiczne myślenie jest wadliwe; po prostu dlatego, że nie ma sposobu, .messagesaby utrzymać swoją scrollToppozycję powyżej maksimum wartość) . Dlatego natychmiast ustawia scrollTopmaksymalną wartość, jaką może podać (co nie jest zaskoczeniem 270.334px).

Co możemy zrobić?

Ponieważ aktualizujemy tylko oldHeightprzy zmianie rozmiaru, możemy sprawdzić, czy to wymuszone przewijanie (lub bardziej poprawnie resize) ma miejsce, a jeśli tak, nie aktualizuj oldScrollTop(ponieważ już to załatwiliśmy resize!) Musimy po prostu porównać oldHeighti aktualną wysokość scrollaby sprawdzić, czy nastąpi wymuszone przewijanie. Działa to, ponieważ warunek, że oldHeightnie jest równy aktualnej wysokości, scrollbędzie spełniony tylko wtedy resize, gdy się zdarzy (co jest przypadkiem, gdy nastąpi wymuszone przewijanie).

Oto kod (w JSFiddle) poniżej:

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>

Testowany na Firefox i Chrome na urządzenia mobilne i działa w obu przeglądarkach.

Richard
źródło