Збереження позиції прокрутки працює лише тоді, коли не знаходиться в нижній частині повідомлень 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. Вони мають значно менші накладні витрати процесора, ніж обробники подій. Якщо ви орієнтовані на старіші веб-переглядачі, обидва мають поліфіли.
безвеликі

Ви пробували це на Firefox для мобільних пристроїв? Здається, ця проблема не має. Однак спроба цього в Chrome викликає згадану вами проблему.
Річард

Ну, це все одно має працювати на Chrome. Приємно, що у Firefox проблеми немає.
Райан Пешель

Моє погано, що я неправильно не передавав свою точку зору. Якщо у одного веб-переглядача є проблема, а в іншого немає, це, 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, оскільки якщо ви знаходитесь біля нижньої частини, вона встановить 0 на scrollподію безпосередньо перед resizeподією, зіпсувавши обчислення.

  2. Ще одна причина, чому всі рішення мали труднощі в нижній частині повідомлень div, є дещо складним для розуміння. Наприклад, у своєму рішенні щодо розміру я просто додаю або віднімаю 250 (висота мобільної клавіатури), scrollTopколи відкриваю або закриваю віртуальну клавіатуру. Це чудово працює, за винятком нижнього дна. Чому? Тому що скажімо, ви знаходитесь на 50 пікселів знизу та закриваєте клавіатуру. Він відніме 250 з scrollTop(висота клавіатури), але він повинен відняти лише 50! Таким чином, він завжди буде скинутий у неправильне фіксоване положення при закритті клавіатури внизу.

  3. Я також вважаю, що ви не можете використовувати onFocusі onBlurподії для цього рішення, оскільки вони трапляються лише під час вибору текстового поля для відкриття клавіатури. Ви чудово вмієте відкривати та закривати мобільну клавіатуру, не активуючи ці події, і як такі вони тут не в змозі використовуватись.

Я вважаю, що вищезазначені моменти важливі для розробки рішення, оскільки вони спочатку не очевидні, але заважають розвиватися надійному рішенню.

Мені не подобається це рішення (інтервал трохи неефективний і схильний до перегонів), але я не можу знайти нічого кращого, що завжди працює.


1

Я думаю, що ти хочеш overflow-anchor

Підтримка зростає, але не загальна, але https://caniuse.com/#feat=css-overflow-anchor

З статті про CSS-трюки про нього:

Прокручування прив’язки запобігає такому «стрибкоподібному» перегляду, блокуючи позицію користувача на сторінці, коли зміни відбуваються в DOM над поточним місцеположенням. Це дозволяє користувачеві триматися на якорі там, де вони знаходяться на сторінці, навіть коли нові елементи завантажуються в DOM.

Властивість переповнення якоря дозволяє нам відмовитися від функції прокрутки прив’язки в тому випадку, якщо бажано дозволити повторному потоку вмісту під час завантаження елементів.

Ось дещо змінена версія одного з їх прикладів:

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висота відхиляється. Для цього необхідно вручну відрегулювати положення прокрутки 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 для мобільних пристроїв, і він працює для обох браузерів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.