Поддержание положения прокрутки работает только когда не в нижней части сообщения div

10

Я пытаюсь имитировать другие мобильные приложения чата, где при выборе send-message текстового поля и открытии виртуальной клавиатуры самое нижнее сообщение все еще отображается. Кажется, не существует способа сделать это с помощью CSS, так что JavaScriptresize (единственный способ выяснить, когда клавиатура открывается и закрывается, очевидно), события и ручная прокрутка для спасения.

Кто-то предоставил это решение, и я узнал это решение , которое, похоже, работает.

За исключением одного случая. По какой-то причине, если вы находитесь в пределахMOBILE_KEYBOARD_HEIGHT (250 пикселей в моем случае) пикселей от нижней части div сообщений, когда вы закрываете мобильную клавиатуру, происходит нечто странное. С прежним решением, оно прокручивается до дна. И с последним решением, он вместо этого прокручивается вверхMOBILE_KEYBOARD_HEIGHT пиксели снизу.

Если вы прокручиваетесь выше этой высоты, оба решения, представленные выше, работают безупречно. Только когда вы находитесь на дне, у них есть эта незначительная проблема.

Я подумал, может быть, это была просто моя программа, вызывающая это с каким-то странным заблудшим кодом, но нет, я даже воспроизвел скрипку, и она имеет именно эту проблему. Приношу свои извинения за то, что это так сложно отлаживать, но если вы перейдете на https://jsfiddle.net/t596hy8d/6/show (суффикс 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-Tricks:

Привязка прокрутки предотвращает такой «прыжок», блокируя положение пользователя на странице, пока происходят изменения в 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из .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 для мобильных устройств и работает для обоих браузеров.

Ричард
источник