การคงตำแหน่งการเลื่อนจะใช้งานได้เฉพาะเมื่อไม่ได้อยู่ใกล้กับด้านล่างของ 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 ต่ำกว่าตัวจัดการเหตุการณ์ หากคุณกำหนดเป้าหมายไปที่เบราว์เซอร์รุ่นเก่าทั้งสองแบบมี polyfills
ใหญ่

คุณลองสิ่งนี้บน 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;
  });
}

ฉันมี epiphanies ระหว่างทาง:

  1. เมื่อปิดแป้นพิมพ์เสมือนจริงscrollเหตุการณ์จะเกิดขึ้นทันทีก่อนที่resizeเหตุการณ์จะเกิดขึ้น ดูเหมือนว่าจะเกิดขึ้นเมื่อปิดแป้นพิมพ์เท่านั้นไม่เปิด นี่คือเหตุผลที่คุณไม่สามารถใช้scrollเหตุการณ์ในการตั้งค่าpxFromBottomเพราะถ้าคุณอยู่ใกล้ด้านล่างมันจะตั้งค่าเป็น 0 ในscrollเหตุการณ์ก่อนresizeเหตุการณ์ทำให้เกิดการคำนวณ

  2. อีกเหตุผลว่าทำไมการแก้ปัญหาทั้งหมดมีปัญหาใกล้ด้านล่างของ div div เป็นเรื่องยากที่จะเข้าใจ ตัวอย่างเช่นในโซลูชันการปรับขนาดของฉันฉันเพิ่งเพิ่มหรือลบ 250 (ความสูงของคีย์บอร์ดมือถือ) scrollTopเมื่อเปิดหรือปิดคีย์บอร์ดเสมือน มันทำงานได้อย่างสมบูรณ์ยกเว้นใกล้ด้านล่าง ทำไม? เพราะสมมุติว่าคุณอยู่ที่ 50 พิกเซลจากด้านล่างและปิดแป้นพิมพ์ มันจะลบ 250 จากscrollTop(ความสูงของแป้นพิมพ์) แต่ควรลบ 50 เท่านั้น! ดังนั้นมันจะถูกรีเซ็ตเป็นตำแหน่งคงที่ผิดเสมอเมื่อปิดแป้นพิมพ์ใกล้กับด้านล่าง

  3. ฉันยังเชื่อว่าคุณไม่สามารถใช้onFocusและonBlurกิจกรรมสำหรับการแก้ปัญหานี้ได้เพราะสิ่งเหล่านั้นจะเกิดขึ้นเฉพาะเมื่อเริ่มเลือกกล่องข้อความเพื่อเปิดแป้นพิมพ์ คุณสามารถเปิดและปิดคีย์บอร์ดมือถือได้อย่างสมบูรณ์แบบโดยไม่ต้องเปิดใช้งานกิจกรรมเหล่านี้ดังนั้นจึงไม่สามารถใช้ที่นี่ได้

ฉันเชื่อว่าประเด็นข้างต้นมีความสำคัญต่อการพัฒนาโซลูชันเนื่องจากไม่ชัดเจนในตอนแรก แต่ป้องกันไม่ให้โซลูชันมีประสิทธิภาพจากการพัฒนา

ฉันไม่ชอบวิธีแก้ปัญหานี้ (ช่วงเวลาค่อนข้างไม่มีประสิทธิภาพและมีแนวโน้มที่จะมีสภาพการแข่งขัน) แต่ฉันไม่สามารถหาสิ่งที่ดีกว่าที่ใช้งานได้


1

ฉันคิดว่าสิ่งที่คุณต้องการคือ overflow-anchor

การสนับสนุนกำลังเพิ่มขึ้น แต่ไม่ใช่ทั้งหมด แต่https://caniuse.com/#feat=css-overflow-anchor

จากบทความ CSS-Tricks เกี่ยวกับมัน:

Scroll Anchoring ป้องกันไม่ให้ประสบการณ์ "กระโดด" โดยการล็อคตำแหน่งของผู้ใช้บนหน้าเว็บในขณะที่มีการเปลี่ยนแปลงเกิดขึ้นใน DOM เหนือตำแหน่งปัจจุบัน สิ่งนี้ช่วยให้ผู้ใช้สามารถยึดตำแหน่งที่พวกเขาอยู่บนหน้าได้แม้ว่าองค์ประกอบใหม่จะถูกโหลดไปยัง DOM

คุณสมบัติโอเวอร์โฟลว์สมอร์ช่วยให้เราสามารถเลือกที่จะไม่ใช้ฟีเจอร์ 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ของ.messagesการoldScrollTopและoldHeightตามลำดับ
  • อัปเดตoldScrollTopและoldHeightทุกครั้งที่resizeเกิดขึ้นwindowและอัปเดตoldScrollTopทุกครั้งที่scrollเกิดขึ้น.messages
  • เมื่อwindowหด (เมื่อแป้นพิมพ์เสมือนจริงแสดง) ความสูงของ.messagesจะหดกลับโดยอัตโนมัติ พฤติกรรมที่ตั้งใจคือการทำให้เนื้อหาส่วนใหญ่ของ.messagesยังคงมองเห็นได้แม้เมื่อ.messagesความสูง retracts นี้ต้องการให้เราปรับเลื่อนตำแหน่งด้วยตนเองของscrollTop.messages
  • เมื่อการแสดงแป้นพิมพ์เสมือนการปรับปรุงscrollTopของ.messagesเพื่อให้แน่ใจว่าเป็นส่วนหนึ่งของใต้ที่สุด.messagesก่อนที่จะเพิกถอนความสูงที่เกิดขึ้นคือยังมองเห็นได้
  • เมื่อหนังแป้นพิมพ์เสมือนการปรับปรุงscrollTopของ.messagesเพื่อให้แน่ใจว่าส่วนใต้ที่สุดของ.messagesซากส่วนใต้ที่สุดของ.messagesหลังจากที่ขยายตัวสูง (ยกเว้นกรณีที่การขยายตัวไม่สามารถเกิดขึ้นขึ้นนี้เกิดขึ้นเมื่อคุณเกือบที่ด้านบนของ.messages)

อะไรทำให้เกิดปัญหา

การคิดเชิงตรรกะของฉัน (เริ่มแรกอาจมีข้อบกพร่อง) คือ: resizeเกิดขึ้น.messages'การเปลี่ยนแปลงความสูงการอัพเดท.messages scrollTopเกิดขึ้นภายในresizeตัวจัดการเหตุการณ์ของเรา อย่างไรก็ตามเมื่อ.messagesมีการขยายตัวของความสูงscrollเหตุการณ์ที่เกิดขึ้นอยากรู้อยากเห็นมาก่อนresize! และยิ่งอยากรู้อยากเห็นscrollเหตุการณ์นั้นเกิดขึ้นก็ต่อเมื่อเราซ่อนแป้นพิมพ์เมื่อเราเลื่อนไปที่scrollTopค่าสูงสุดเมื่อ.messagesไม่หดกลับ ในกรณีของฉันที่นี้หมายถึงว่าเมื่อผมเลื่อนด้านล่าง270.334px(สูงสุดscrollTopก่อนที่จะ.messagesมีการหด) และซ่อนแป้นพิมพ์ที่แปลกscrollก่อนresizeเหตุการณ์ที่เกิดขึ้นและเลื่อนของคุณให้ตรง.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 สำหรับมือถือและใช้งานได้กับเบราว์เซอร์ทั้งสอง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.