Il mantenimento della posizione di scorrimento funziona solo quando non vicino alla parte inferiore dei messaggi div


10

Sto cercando di imitare altre app di chat mobile in cui quando si seleziona la send-messagecasella di testo e si apre la tastiera virtuale, il messaggio più in basso è ancora in vista. Non sembra esserci un modo per farlo sorprendentemente con i CSS, quindi JavaScript resize(l'unico modo per scoprire quando la tastiera è aperta e apparentemente chiusa) eventi e lo scorrimento manuale per il salvataggio.

Qualcuno ha fornito questa soluzione e ho scoperto questa soluzione , che sembrano funzionare entrambe.

Tranne in un caso. Per qualche motivo, se ti trovi entro MOBILE_KEYBOARD_HEIGHT(250 pixel nel mio caso) pixel della parte inferiore dei div dei messaggi, quando chiudi la tastiera del cellulare, succede qualcosa di strano. Con la prima soluzione, scorre verso il basso. E con quest'ultima soluzione, scorre invece i MOBILE_KEYBOARD_HEIGHTpixel verso l' alto dal basso.

Se si scorre sopra questa altezza, entrambe le soluzioni fornite sopra funzionano in modo impeccabile. È solo quando sei vicino al fondo che hanno questo piccolo problema.

Ho pensato che forse era solo il mio programma a causare questo con uno strano codice randagio, ma no, ho persino riprodotto un violino e ha questo problema esatto. Mi scuso per aver reso il debug così difficile, ma se vai su https://jsfiddle.net/t596hy8d/6/show (il suffisso show fornisce una modalità a schermo intero) sul tuo telefono, dovresti essere in grado di vedere il stesso comportamento.

Tale comportamento essendo, se scorri verso l'alto, l'apertura e la chiusura della tastiera mantiene la posizione. Tuttavia, se chiudi la tastiera entro MOBILE_KEYBOARD_HEIGHTpixel della parte inferiore, scoprirai che scorre invece verso la parte inferiore.

Cosa sta causando questo?

Riproduzione codice qui:

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>


Vorrei sostituire i gestori di eventi con IntersectionObserver e ResizeObserver. Hanno un sovraccarico della CPU molto inferiore rispetto ai gestori di eventi. Se stai prendendo di mira i browser più vecchi, entrambi hanno polyfill.
bigless

Hai provato questo su Firefox per dispositivi mobili? Non sembra avere questo problema. Tuttavia, provarlo su Chrome provoca il problema che hai citato.
Richard

Beh, deve funzionare comunque su Chrome. È bello che Firefox non abbia il problema però.
Ryan Peschel, il

Il mio male per non aver trasmesso correttamente il mio punto. Se un browser ha un problema e l'altro non lo fa, questo, IMO, potrebbe significare che si potrebbe essere necessario disporre di implementazione leggermente diversa per i vari browser.
Richard

1
@halfer Va bene. Vedo. Grazie per il promemoria, terrò conto di ciò la prossima volta che chiederò a qualcuno di rivisitare una risposta.
Richard

Risposte:


3

Ho finalmente trovato una soluzione che funziona davvero . Anche se potrebbe non essere l'ideale, in realtà funziona in tutti i casi. Ecco il codice:

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

Alcune epifanie che ho avuto lungo la strada:

  1. Quando si chiude la tastiera virtuale, scrollsi verifica immediatamente un evento prima resizedell'evento. Questo sembra accadere solo chiudendo la tastiera, non aprendola. Questo è il motivo per cui non è possibile utilizzare l' scrollevento per impostare pxFromBottom, perché se ci si trova in fondo si imposterà su 0 scrollnell'evento proprio prima resizedell'evento, incasinando il calcolo.

  2. Un altro motivo per cui tutte le soluzioni hanno avuto difficoltà nella parte inferiore dei messaggi div è un po 'difficile da capire. Ad esempio, nella mia soluzione di ridimensionamento aggiungo o sottraggo 250 (altezza della tastiera del cellulare) scrollTopquando apro o chiudo la tastiera virtuale. Funziona perfettamente tranne vicino al fondo. Perché? Perché supponiamo che tu sia a 50 pixel dal basso e chiudi la tastiera. Sottrarrà 250 da scrollTop(l'altezza della tastiera), ma dovrebbe sottrarre solo 50! Quindi si ripristinerà sempre nella posizione fissa errata quando si chiude la tastiera vicino al fondo.

  3. Credo anche che non puoi utilizzare eventi onFocused onBlureventi per questa soluzione, perché si verificano solo quando si seleziona inizialmente la casella di testo per aprire la tastiera. Sei perfettamente in grado di aprire e chiudere la tastiera del cellulare senza attivare questi eventi e, in quanto tale, non possono essere utilizzati qui.

Credo che i punti di cui sopra siano importanti per lo sviluppo di una soluzione, perché inizialmente non sono ovvi, ma impediscono lo sviluppo di una soluzione solida.

Non mi piace questa soluzione (l'intervallo è un po 'inefficiente e incline alle condizioni di gara), ma non riesco a trovare niente di meglio che funzioni sempre.


1

Penso che quello che vuoi sia overflow-anchor

Il supporto è in aumento, ma non totale, ma https://caniuse.com/#feat=css-overflow-anchor

Da un articolo CSS-Tricks su di esso:

L'ancoraggio a scorrimento impedisce quell'esperienza di "salto" bloccando la posizione dell'utente sulla pagina mentre sono in corso modifiche nel DOM sopra la posizione corrente. Ciò consente all'utente di rimanere ancorato nel punto in cui si trova sulla pagina anche quando vengono caricati nuovi elementi nel DOM.

La proprietà overflow-anchor ci consente di annullare la funzione di ancoraggio a scorrimento nel caso in cui si preferisca consentire il ri-flusso del contenuto quando gli elementi vengono caricati.

Ecco una versione leggermente modificata di uno dei loro esempi:

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>

Apri questo sul cellulare: https://cdpn.io/chasebank/debug/PowxdOR

Quello che sta facendo è sostanzialmente disabilitare qualsiasi ancoraggio predefinito dei nuovi elementi del messaggio, con #scroller * { overflow-anchor: none }

E invece ancorare un elemento vuoto #anchor { overflow-anchor: auto }che verrà sempre dopo quei nuovi messaggi, poiché i nuovi messaggi vengono inseriti prima di esso.

Deve esserci una pergamena per notare un cambiamento nell'ancoraggio, che penso sia generalmente una buona UX. In ogni caso, l'attuale posizione di scorrimento dovrebbe essere mantenuta quando si apre la tastiera.


0

La mia soluzione è la stessa della soluzione proposta con un'aggiunta del controllo condizionale. Ecco una descrizione della mia soluzione:

  • Registrare l'ultima posizione di scorrimento scrollTope l'ultima clientHeightdi .messagesa oldScrollTope oldHeightrispettivamente
  • Aggiorna oldScrollTope oldHeightogni volta che resizesuccede windowe aggiorna oldScrollTopogni volta che scrollsuccede.messages
  • Quando windowviene ridotto (quando viene visualizzata la tastiera virtuale), l'altezza di .messagesverrà automaticamente ritratta. Il comportamento previsto è di rendere .messagesancora visibile il contenuto più in basso anche quando .messagesl'altezza si ritrae. Ciò richiede di regolare manualmente la posizione scrollTopdi scorrimento di .messages.
  • Quando viene visualizzata la tastiera virtuale, aggiornare scrollTopdi .messagesper assicurarsi che la parte più in basso .messagesprima che avvenga la sua retrazione in altezza sia ancora visibile
  • Quando la tastiera virtuale si nasconde, esegui l'aggiornamento scrollTopdi .messagesper assicurarti che la parte più in basso .messagesrimanga la parte più in basso .messagesdopo l'espansione dell'altezza (a meno che l'espansione non possa avvenire verso l'alto; questo accade quando sei quasi in cima .messages)

Cosa ha causato il problema?

Il mio pensiero logico (possibilmente iniziale imperfetto) è: resizesuccede, .messages'l'altezza cambia, l'aggiornamento .messages scrollTopavviene nel nostro resizegestore di eventi. Tuttavia, al momento .messagesdell'espansione dell'altezza, un scrollevento accade curiosamente prima di un resize! E ancora più curioso, l' scrollevento si verifica solo quando nascondiamo la tastiera quando siamo passati sopra il scrollTopvalore massimo di quando .messagesnon viene ritirato. Nel mio caso, questo significa che quando ho scorrere al di sotto 270.334px(il massimo scrollTop, prima .messagesè retratto) e nascondere la tastiera, che strano scrollprima di resizeevento si verifica e scorre la vostra .messagesesattamente 270.334px. Questo ovviamente incasina la nostra soluzione sopra.

Fortunatamente, possiamo aggirare questo. La mia deduzione personale del perché questo scrollprima che resizesi verifichi l' evento è perché .messagesnon riesco a mantenere la sua scrollTopposizione sopra 270.334pxquando si espande in altezza (questo è il motivo per cui ho detto che il mio pensiero logico iniziale è difettoso; semplicemente perché non c'è modo .messagesdi mantenere la sua scrollTopposizione al di sopra del suo massimo valore) . Pertanto, imposta immediatamente scrollTopil valore massimo che può dare (che è, ovviamente, 270.334px).

Cosa possiamo fare?

Poiché eseguiamo l'aggiornamento solo oldHeightal ridimensionamento, possiamo verificare se questo scorrimento forzato (o più correttamente resize) si verifica e, in caso affermativo, non aggiornare oldScrollTop(perché lo abbiamo già gestito resize!) Dobbiamo semplicemente confrontare oldHeighte l'altezza corrente su scrollper vedere se si verifica questo scorrimento forzato. Questo funziona perché la condizione di oldHeightnon essere uguale all'altezza attuale scrollè vera solo quando resizeaccade (che è una coincidenza quando avviene lo scorrimento forzato).

Ecco il codice (in JSFiddle) di seguito:

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>

Testato su Firefox e Chrome per dispositivi mobili e funziona con entrambi i browser.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.