Das Beibehalten der Bildlaufposition funktioniert nur, wenn Sie sich nicht am unteren Rand des Nachrichtendivs befinden

10

Ich versuche, andere mobile Chat-Apps nachzuahmen. Wenn Sie das send-messageTextfeld auswählen und die virtuelle Tastatur öffnen, wird die unterste Meldung weiterhin angezeigt. Es scheint erstaunlicherweise keine Möglichkeit zu geben, dies mit CSS zu tun, also JavaScript- resizeEreignisse (nur um herauszufinden, wann die Tastatur offenbar geöffnet und geschlossen wird) und manuelles Scrollen zur Rettung.

Jemand hat diese Lösung bereitgestellt , und ich habe diese Lösung herausgefunden , die beide zu funktionieren scheinen.

Außer in einem Fall. Aus irgendeinem Grund MOBILE_KEYBOARD_HEIGHTpassiert etwas Seltsames , wenn Sie sich innerhalb (250 Pixel in meinem Fall) Pixel am unteren Rand des Nachrichtendivs befinden, wenn Sie die mobile Tastatur schließen. Bei der ersteren Lösung wird nach unten gescrollt. Bei der letzteren Lösung werden stattdessen MOBILE_KEYBOARD_HEIGHTPixel von unten nach oben gescrollt.

Wenn Sie über diese Höhe gescrollt werden, funktionieren beide oben angegebenen Lösungen einwandfrei. Nur wenn Sie sich ganz unten befinden, haben sie dieses kleine Problem.

Ich dachte, vielleicht war es nur mein Programm, das dies mit einem seltsamen Streucode verursachte, aber nein, ich habe sogar eine Geige reproduziert und es hat genau dieses Problem. Ich entschuldige mich dafür, dass das Debuggen so schwierig ist. Wenn Sie jedoch auf Ihrem Telefon zu https://jsfiddle.net/t596hy8d/6/show (das Show-Suffix bietet einen Vollbildmodus) gehen, sollten Sie das sehen können gleiches Verhalten.

Wenn Sie genug nach oben scrollen, bleibt beim Öffnen und Schließen der Tastatur die Position erhalten. Wenn Sie jedoch in der Nähe der Tastatur innerhalb MOBILE_KEYBOARD_HEIGHTPixel des Bodens, werden Sie feststellen , dass es nach unten scrollt statt.

Was verursacht das?

Code-Reproduktion hier:

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
quelle
Ich würde Event-Handler durch IntersectionObserver und ResizeObserver ersetzen. Sie haben einen viel geringeren CPU-Overhead als Event-Handler. Wenn Sie auf ältere Browser abzielen, verfügen beide über Polyfills.
Bigless
Haben Sie dies auf Firefox für mobile Geräte versucht? Es scheint dieses Problem nicht zu haben. Wenn Sie dies jedoch in Chrome versuchen, tritt das von Ihnen erwähnte Problem auf.
Richard
Nun, es muss sowieso auf Chrome funktionieren. Das ist schön, dass Firefox das Problem nicht hat.
Ryan Peschel
Mein schlechtes, weil ich meinen Standpunkt nicht richtig vermittelt habe. Wenn ein Browser hat ein Problem und eine andere nicht, dieses, IMO, könnte bedeuten , dass Sie möglicherweise etwas andere Implementierung für verschiedene Browser haben müssen.
Richard
1
@halfer Okay. Aha. Vielen Dank für die Erinnerung, ich werde dies berücksichtigen, wenn ich das nächste Mal jemanden auffordere, eine Antwort erneut zu prüfen.
Richard

Antworten:

3

Ich habe endlich eine Lösung gefunden, die tatsächlich funktioniert. Obwohl es möglicherweise nicht ideal ist, funktioniert es tatsächlich in allen Fällen. Hier ist der Code:

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

Einige Offenbarungen, die ich unterwegs hatte:

  1. Beim Schließen der virtuellen Tastatur scrolltritt unmittelbar vor dem resizeEreignis ein Ereignis auf . Dies scheint nur beim Schließen der Tastatur zu geschehen, nicht beim Öffnen. Dies ist der Grund, warum Sie das scrollEreignis nicht zum Festlegen verwenden können pxFromBottom, da es sich im scrollEreignis unmittelbar vor dem resizeEreignis auf 0 setzt , wenn Sie sich in der Nähe des unteren Bereichs befinden, was die Berechnung durcheinander bringt .

  2. Ein weiterer Grund, warum alle Lösungen am Ende des Nachrichtendivs Schwierigkeiten hatten, ist etwas schwierig zu verstehen. Zum Beispiel addiere oder subtrahiere ich in meiner Größenänderungslösung nur 250 (Höhe der mobilen Tastatur), scrollTopwenn ich die virtuelle Tastatur öffne oder schließe. Dies funktioniert perfekt, außer in der Nähe des Bodens. Warum? Nehmen wir an, Sie sind 50 Pixel von unten entfernt und schließen die Tastatur. Es werden 250 von scrollTop(der Tastaturhöhe) subtrahiert, aber es sollten nur 50 subtrahiert werden! Daher wird es beim Schließen der Tastatur in der Nähe der Unterseite immer auf die falsche feste Position zurückgesetzt.

  3. Ich glaube auch, dass Sie onFocusund onBlurEreignisse für diese Lösung nicht verwenden können , da diese nur auftreten, wenn Sie zunächst das Textfeld zum Öffnen der Tastatur auswählen. Sie können die mobile Tastatur problemlos öffnen und schließen, ohne diese Ereignisse zu aktivieren. Daher können sie hier nicht verwendet werden.

Ich glaube, die oben genannten Punkte sind wichtig für die Entwicklung einer Lösung, da sie zunächst nicht offensichtlich sind, aber die Entwicklung einer robusten Lösung verhindern.

Ich mag diese Lösung nicht (das Intervall ist etwas ineffizient und anfällig für Rennbedingungen), aber ich kann nichts Besseres finden, das immer funktioniert.

Ryan Peschel
quelle
1

Ich denke was du willst ist overflow-anchor

Der Support nimmt zu, ist aber noch nicht vollständig. Https://caniuse.com/#feat=css-overflow-anchor

Aus einem CSS-Tricks-Artikel dazu:

Die Bildlaufverankerung verhindert das "Springen", indem die Position des Benutzers auf der Seite gesperrt wird, während Änderungen im DOM über dem aktuellen Speicherort vorgenommen werden. Auf diese Weise kann der Benutzer auch dann auf der Seite verankert bleiben, wenn neue Elemente in das DOM geladen werden.

Mit der Eigenschaft "Überlaufanker" können wir die Funktion "Bildlaufverankerung" deaktivieren, falls der Inhalt beim Laden von Elementen bevorzugt erneut fließen soll.

Hier ist eine leicht modifizierte Version eines ihrer Beispiele:

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>

Öffnen Sie dies auf dem Handy: https://cdpn.io/chasebank/debug/PowxdOR

Grundsätzlich wird die Standardverankerung der neuen Nachrichtenelemente mit deaktiviert #scroller * { overflow-anchor: none }

Und stattdessen ein leeres Element verankern #anchor { overflow-anchor: auto }, das immer nach diesen neuen Nachrichten kommt, da die neuen Nachrichten davor eingefügt werden .

Es muss eine Schriftrolle geben, um eine Änderung der Verankerung zu bemerken, was meiner Meinung nach im Allgemeinen eine gute UX ist. In beiden Fällen sollte die aktuelle Bildlaufposition beim Öffnen der Tastatur beibehalten werden.

Verfolgungsjagd
quelle
0

Meine Lösung ist die gleiche wie Ihre vorgeschlagene Lösung mit einer zusätzlichen bedingten Prüfung. Hier ist eine Beschreibung meiner Lösung:

  • Nehmen Sie die letzte Scroll - Position scrollTopund zuletzt clientHeightvon .messageszu oldScrollTopund oldHeightjeweils
  • Aktualisieren oldScrollTopund oldHeightjedes Mal, wenn ein resizepassiert, windowund oldScrollTopjedes Mal, wenn ein scrollpassiert.messages
  • Wenn windowes verkleinert wird (wenn die virtuelle Tastatur angezeigt wird), wird die Höhe .messagesautomatisch zurückgezogen. Das beabsichtigte Verhalten besteht darin, den untersten Inhalt von auch dann .messagesnoch sichtbar zu machen, wenn sich .messagesdie Höhe zurückzieht. Dies erfordert, dass wir die Bildlaufposition scrollTopvon manuell anpassen .messages.
  • Wenn die virtuelle Tastatur angezeigt wird , wird die Aktualisierung scrollTopvon .messagesangezeigt, um sicherzustellen, dass der unterste Teil .messagesvor dem Zurückziehen der Höhe weiterhin sichtbar ist
  • Wenn die virtuelle Tastatur versteckt, Aktualisierung scrollTopvon .messagesum sicherzustellen , dass der unterste Teil der .messagesÜberreste des unterste Teil des .messagesnach der Höhenausdehnung (es sei denn , Expansion kann nicht nach oben passieren, das passiert , wenn man fast an der Spitze ist .messages)

Was hat das Problem verursacht?

Mein (anfänglich möglicherweise fehlerhaftes) logisches Denken lautet: resizepassiert, .messages'Höhenänderungen, Aktualisierung .messages scrollToperfolgt in unserem resizeEvent-Handler. Bei .messageseiner scrollHöhenerweiterung tritt jedoch seltsamerweise ein Ereignis vor einem resize! Und noch merkwürdiger ist, dass das scrollEreignis nur auftritt, wenn wir die Tastatur ausblenden, wenn wir über den Maximalwert gescrollt haben, scrollTopwenn .messagesnicht zurückgezogen wird. In meinem Fall bedeutet dies, dass, wenn ich nach unten scrolle 270.334px(das Maximum scrollTopvor dem .messagesZurückziehen) und die Tastatur ausblende, das seltsam ist, scrollbevor ein resizeEreignis eintritt und Sie .messagesgenau zu scrollen 270.334px. Dies bringt unsere obige Lösung offensichtlich durcheinander.

Glücklicherweise können wir das umgehen. Mein persönlicher Schluss, warum dies scrollvor dem resizeEreignis geschieht, ist, dass ich.messages seine scrollTopPosition von oben nicht beibehalten kann, 270.334pxwenn es sich in der Höhe ausdehnt (aus diesem Grund habe ich erwähnt, dass mein anfängliches logisches Denken fehlerhaft ist; einfach, weil es keine Möglichkeit gibt .messages, seine scrollTopPosition über seinem Maximum zu halten Wert) . Daher setzt es sofort scrollTopden Maximalwert, den es geben kann (was nicht überraschend ist 270.334px).

Was können wir tun?

Da wir nur beim Ändern der oldHeightGröße aktualisieren , können wir überprüfen, ob dieser erzwungene Bildlauf (oder korrekter resize) erfolgt, und wenn dies der Fall ist, nicht aktualisieren oldScrollTop(da wir dies bereits erledigt haben resize!). Wir müssen lediglich vergleichen oldHeightund die aktuelle Höhe aktivieren scrollum zu sehen, ob dieses erzwungene Scrollen passiert. Dies funktioniert, weil die Bedingung, oldHeightnicht gleich der aktuellen Höhe zu scrollsein, nur dann erfüllt ist, wenn resizedies geschieht (was zufällig der Fall ist, wenn dieses erzwungene Scrollen auftritt).

Hier ist der Code (in JSFiddle) unten:

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>

Getestet auf Firefox und Chrome für Handys und funktioniert für beide Browser.

Richard
quelle