스크롤 위치 유지는 메시지 div의 하단에 있지 않을 때만 작동합니다.


10

send-message텍스트 상자 를 선택 하고 가상 키보드를 열 때 가장 아래쪽 메시지가 여전히 표시 되는 다른 모바일 채팅 앱을 모방하려고합니다 . CSS로 놀랍게도 그렇게 할 수있는 방법이없는 것 같습니다. 따라서 JavaScript resize(키보드가 열리고 닫히는 시점을 알 수있는 방법 만) 이벤트와 수동으로 구조를 스크롤합니다.

누군가가 제공하는 이 솔루션을 나는 발견 이 솔루션 이 모두 작동하는 것.

한 경우를 제외하고. 에 대한 일부 당신이 내 경우 이유 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 오버 헤드가 훨씬 적습니다. 구형 브라우저를 타겟팅하는 경우 둘 다 폴리 필이 있습니다.
bigless

휴대 기기 용 Firefox에서 사용해 보셨습니까? 이 문제가없는 것 같습니다. 그러나 Chrome에서 이것을 시도하면 언급 한 문제가 발생합니다.
리차드

어쨌든 Chrome에서 작동해야합니다. Firefox에는 문제가 없습니다.
Ryan Peschel

내 요점을 제대로 전달하지 못한 것이 나쁘다. 하나의 브라우저에 문제가 다른 하나는,이, IMO,하지 않는 경우 는 것을 의미 할 수있다 다른 브라우저에 대한 약간 다른 구현이 필요합니다.
리처드

1
@halfer 좋습니다. 내가 참조. 다음에 누군가에게 답변을 다시 요청할 때이를 고려하겠습니다.
리처드

답변:


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이벤트 직전 이벤트 에서 0으로 설정 resize되어 계산이 엉망이 되기 때문 입니다.

  2. 모든 솔루션이 메시지 div의 하단 근처에서 어려움을 겪었던 또 다른 이유는 이해하기가 약간 까다 롭습니다. 예를 들어, 크기 조정 솔루션scrollTop 에서 가상 키보드를 열거 나 닫을 때 250 (모바일 키보드 높이)을 더하거나 뺍니다 . 바닥 근처를 제외하고는 완벽하게 작동합니다. 왜? 하단에서 50 픽셀 떨어져 있고 키보드를 닫았다 고 가정 해 봅시다. scrollTop(키보드 높이) 에서 250을 빼지 만 50 만 빼야합니다! 따라서 하단 근처에서 키보드를 닫을 때 항상 잘못된 고정 위치로 재설정됩니다.

  3. 또한 이 솔루션에 사용할 수없는 이벤트 onFocusonBlur이벤트는 키보드를 열 텍스트 상자를 처음 선택할 때만 발생하기 때문에 믿습니다 . 이러한 이벤트를 활성화하지 않고 모바일 키보드를 열거 나 닫을 수 있으므로 여기에서 사용할 수 없습니다.

위의 사항은 처음에는 분명하지 않지만 강력한 솔루션이 개발되지 못하므로 솔루션 개발에 중요하다고 생각합니다.

나는이 솔루션을 좋아하지 않습니다 (간격은 약간 비효율적이며 경쟁 조건에 취약합니다). 그러나 항상 작동하는 더 좋은 것을 찾을 수는 없습니다.


1

당신이 원하는 것은 overflow-anchor

지원은 늘어나고 있지만 아직은 아니지만 https://caniuse.com/#feat=css-overflow-anchor

A로부터 그것에 CSS-트릭 기사 :

Scroll Anchoring은 현재 위치 위의 DOM에서 변경이 수행되는 동안 페이지에서 사용자의 위치를 ​​잠 가서 "점프"경험을 방지합니다. 이를 통해 사용자는 새로운 요소가 DOM에로드 되더라도 페이지에있는 위치에 고정 된 상태를 유지할 수 있습니다.

overflow-anchor 속성을 사용하면 요소를로드 할 때 내용을 리플 로우 할 수있는 경우 Scroll Anchoring 기능을 옵트 아웃 할 수 있습니다.

다음은 예제 중 하나의 약간 수정 된 버전입니다.

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각각
  • 최신 정보 oldScrollTopoldHeightA는 모든 시간 resize에 발생 window하고 업데이트 oldScrollTopA는 모든 시간 scroll에 일을.messages
  • 언제 window가 축소 (가상 키보드가 표시 될 때) 높이 .messages가 자동으로 줄어 듭니다 . 의도 된 동작은 높이가 줄어든 .messages경우에도 맨 아래 내용을 계속 표시하는 것 .messages입니다. 이 수동으로 스크롤 위치 조정하기 위해 우리를 필요로 scrollTop의를.messages .
  • 때 가상 키보드 프로그램, 갱신 scrollTop의는 .messages의 맨 아래 부분이 있는지 확인하는 .messages높이의 수축이 발생하기 전에 여전히 볼 수 있습니다
  • 때 가상 키보드 가죽이 업데이트 scrollTop.messages 있는지 확인하기 위해 그것의 맨 아래 부분 .messages유적의 맨 아래 부분 .messages높이 확장 후 (확장이 위쪽으로 발생하지 않는 한, 당신은 상단에 거의있을 때 이런 일이 발생 .messages)

문제의 원인은 무엇입니까?

나의 (초기 적으로 결함이있는) 논리적 사고는 다음과 같습니다 : resizeevents .messages높이 .messages scrollTop내 에서 발생 resize합니다. 그러나, .messages'높이 확장 시 scroll이벤트는 resize! 더욱 궁금한 점은 scroll이벤트 가 취소 되지 않을 때 의 최대 값 이상으로 스크롤했을 때 키보드를 숨길 때만 발생합니다 . 내 경우,이 수단이 나는 아래에 스크롤 할 때 (최대가 전 후진)하고, 키보드를 숨길 이상한 그scrollTop.messages270.334pxscrollTop.messagesscrollresize 이벤트가 발생 하기 전에.messages 하고 정확하게로 스크롤 한다는 것을 의미합니다 270.334px. 이것은 분명히 우리의 솔루션을 위태롭게합니다.

다행히도이 문제를 해결할 수 있습니다. 이것이 왜 내 개인 공제 scroll전과 resize때문에 이벤트 발생은 .messages그것의 유지할 수 scrollTop위의 위치 270.334px는 높이 확장하는 경우 에 대한 방법이 없다 간단하기 때문에, 나는 내 최초의 논리적 사고가 결함이 있음을 언급 한 이유는 ( .messages그 유지하는 scrollTop최대 위에 위치 가치) . 따라서 즉시 scrollTop제공 할 수있는 최대 값으로 설정합니다 .270.334px )으로 설정합니다.

우리는 무엇을 할 수 있습니까?

oldHeight크기 조정시 에만 업데이트하기 때문에이 강제 스크롤 (또는보다 정확하게 resize)이 발생하는지 확인하고 업데이트 된 경우 업데이트하지 마십시오 oldScrollTop(이미 이미 처리했기 때문에 resize!) 단순히 비교 oldHeight하고 현재 높이를 scroll이 강제 스크롤이 발생하는지 확인하십시오. 이것은 oldHeight현재 높이와 같지 않은 조건이 발생할 scroll때만 적용되기 때문에 작동 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 for mobile에서 테스트되었으며 두 브라우저 모두에서 작동합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.