スクロール位置の維持は、メッセージdivの下部近くにない場合にのみ機能します


10

私は他のモバイルチャットアプリを模倣しようとしています。send-messageテキストボックスを選択して仮想キーボードを開くと、一番下のメッセージがまだ表示されています。CSSでこれを驚くほど実行する方法はないようです。そのため、JavaScript resize(明らかにキーボードが開いているときと閉じているときを確認する唯一の方法)イベントと手動でスクロールして救助します。

誰かが提供するこのソリューションを、私は見つけたこのソリューションの両方が動作するように見えます。

1つの場合を除いて。以下のためにいくつかのあなたが以内であれば理由、MOBILE_KEYBOARD_HEIGHT(私の場合は250個のピクセル)メッセージのdivの下のピクセルモバイルキーボードを閉じると、奇妙な何かが起こります。前者のソリューションでは、一番下までスクロールします。後者のソリューションでは、代わりにMOBILE_KEYBOARD_HEIGHTピクセルを下から上にスクロールします。

この高さより上にスクロールすると、上に提供された両方のソリューションが問題なく機能します。彼らがこの小さな問題を抱えているのは、あなたが底辺りにいるときだけです。

多分それは私のプログラムがこれを引き起こしているいくつかの奇妙な迷子のコードだと思っていましたが、いいえ、私はフィドルを再現しさえしており、それはまさにこの問題を抱えています。これをデバッグするのが非常に困難になったことをお詫びしますが、電話でhttps://jsfiddle.net/t596hy8d/6/show(ショーのサフィックスは全画面モードを提供します)にアクセスすると、同じ動作。

その動作は、十分に上にスクロールすると、キーボードの開閉によって位置が維持されます。ただし、下部のピクセル内でキーボードを閉じるとMOBILE_KEYBOARD_HEIGHT代わりに下部にスクロールすることがわかります。

これが原因ですか?

ここでのコードの再現:

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>


イベントハンドラーをIntersectionObserverとResizeObserverに置き換えます。イベントハンドラよりもCPUオーバーヘッドがはるかに低くなります。古いブラウザをターゲットにしている場合は、両方にポリフィルがあります。
ビッグレス

モバイルデバイス用Firefoxでこれを試しましたか?この問題はないようです。ただし、Chromeでこれを試すと、あなたが言及した問題が発生します。
リチャード

まあ、それはとにかくChromeで動作する必要があります。Firefoxに問題がないのは良いことです。
Ryan Peschel

自分の言い分をきちんと伝えられなかったのが悪い。1つのブラウザに問題があり、もう一つは、これは、IMO、しない場合は可能性があなたがいることを意味する別のブラウザのためにわずかに異なる実装を持っている必要があります。
Richard

1
@halferよし。そうですか。リマインダーをありがとう、次回誰かに答えを尋ねるとき、それを考慮に入れます。
Richard

回答:


3

ようやく実際に機能する解決策を見つけました。理想的ではないかもしれませんが、実際にはすべてのケースで機能します。これがコードです:

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

私が途中で持っていたいくつかのエピファニー:

  1. 仮想キーボードを閉じると、scrollイベントの直前にイベントが発生しますresize。これは、キーボードを閉じたときにのみ発生し、開いていないようです。これは、scrollイベントを使用してを設定できない理由です。pxFromBottom下部に近づくと、scrollイベントの直前のresizeイベントでそれ自体が0に設定され、計算が台無しになるためです。

  2. すべての解決策がメッセージdivの下部近くで困難であったもう1つの理由は、理解するのが少し難しいです。たとえば、私のサイズ変更ソリューションでscrollTop、仮想キーボードを開いたり閉じたりするときに250(モバイルキーボードの高さ)を加算または減算するだけです。これは下部を除いて完全に機能します。どうして?下から50ピクセル離れていて、キーボードを閉じているとしましょう。scrollTop(キーボードの高さ)から250を減算しますが、50を減算するだけです!したがって、キーボードを下部近くで閉じると、常に間違った固定位置にリセットされます。

  3. また、テキストボックスを選択してキーボードを開くときにのみ発生するため、このソリューションではonFocusand onBlurイベントを使用できないと思います。これらのイベントをアクティブ化することなく、モバイルキーボードを完全に開閉することができるため、ここでは使用できません。

上記の点は、最初は自明ではないものの、堅牢なソリューションの開発を妨げているため、ソリューションの開発には重要であると思います。

私はこのソリューションが好きではありません(間隔は少し非効率的であり、競合状態になりがちです)が、常に機能するより良いものを見つけることができません。


1

あなたが欲しいのは overflow-anchor

サポートは増えていますが、全体ではありませんが、https://caniuse.com/#feat=css-overflow-anchor

その上にCSS-トリックの記事:

スクロールアンカーは、現在の場所の上のDOMで変更が行われている間、ページ上のユーザーの位置をロックすることにより、その「ジャンプ」エクスペリエンスを防ぎます。これにより、新しい要素がDOMに読み込まれた場合でも、ユーザーはページのどこにでもアンカーされたままになります。

要素がロードされるときにコンテンツをリフローできるようにすることが望ましい場合、overflow-anchorプロパティを使用して、スクロールアンカー機能をオプトアウトできます。

次に、これらの例の1つを少し変更したバージョンを示します。

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>

これをモバイルで開きますhttps : //cdpn.io/chasebank/debug/PowxdOR

これは、基本的に、新しいメッセージ要素のデフォルトのアンカーを無効にすることです。 #scroller * { overflow-anchor: none }

その代わりに#anchor { overflow-anchor: auto }、新しいメッセージがそのに挿入されているため、常に新しいメッセージの後に来る空の要素を固定します。

アンカーの変更に気づくにはスクロールが必要ですが、これは一般的に優れたUXだと思います。ただし、どちらの方法でも、キーボードが開いたときに現在のスクロール位置を維持する必要があります。


0

私の解決策は提案された解決策と同じですが、条件付きチェックが追加されています。これが私の解決策の説明です:

  • 最後のスクロール位置を記録scrollTopし、最後clientHeight.messagesoldScrollTopoldHeightそれぞれ
  • アップデートoldScrollTopoldHeight毎回resizeで発生windowし、更新するoldScrollTopたびscrollに起こるを.messages
  • 場合window(ときに、仮想キーボード番組)収縮させ、高さが.messages自動的に後退します。意図された動作は、高さが後退し.messagesても、一番下のコンテンツが表示されるようにすること.messagesです。これには、のスクロール位置を手動で調整する必要がscrollTopあり.messagesます。
  • 仮想キーボードが表示されたら、の更新scrollTopにより、高さが収縮する前.messagesの最下部.messagesがまだ表示されていることを確認します
  • 仮想キーボードの皮、更新する場合scrollTopのは.messages確かの最下部ことを確認するために.messages遺跡の最下部.messagesの高さの拡張後(拡大は上向きに起こることができない場合を除き、あなたがの上部にはほとんどしている場合に発生します.messages

問題の原因は何ですか?

私の(おそらく欠陥が初期)論理的思考は以下のとおりですresize、起こる.messagesの更新は、「高さの変化を.messages scrollTop当社の内部で起こるresizeイベントハンドラ。ただし、.messages「高さを拡張すると、scroll奇妙なことにresize!さらに興味深いことに、このscrollイベントは、が格納されていないときの最大値を超えてスクロールしたときにキーボードを非表示にしたときにのみ発生します。私は下のスクロールすると私の場合、この手段は(最大前は奇妙なことに、引き込まれる)とキーボードを非表示する前にイベントが発生し、あなたのスクロールを正確に。これは明らかに上のソリューションを台無しにします。scrollTop.messages270.334pxscrollTop.messagesscrollresize.messages270.334px

幸い、これを回避することができます。これは、なぜ私の個人的な控除scroll前のresizeため、イベントが起こるがあり.messages、その維持できないscrollTopの上の位置を270.334px、それが高さに拡大したときのための方法はありませんという理由だけで、私は私の最初の論理的な思考が欠陥があることを言及した理由です(.messagesその維持するscrollTopその最大の上に位置が値)。したがって、すぐにscrollTop最大値に設定されます(当然のことですが270.334px)。

私たちは何ができる?

我々は唯一の更新なのでoldHeight、サイズ変更に(より正確または、この強制スクロールがあれば、我々は確認することができますresize)発生し、それがない場合は、更新されませんoldScrollTop(私たちはすでにでていることに扱われているので、resize我々は単純に比較する必要があります!)oldHeightとの現在の高さscrollこの強制スクロールが発生するかどうかを確認します。これが機能oldHeightするのは、現在の高さに等しくないという条件が発生したscroll場合にのみtrueにresizeなるためです(これは、強制スクロールが発生したときに偶然に発生します)。

以下が(JSFiddleの)コードです。

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>

Firefoxおよびモバイル用Chromeでテストされ、両方のブラウザーで動作します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.