代码之家  ›  专栏  ›  技术社区  ›  Ryan Peschel

保持滚动位置仅在不靠近消息div底部时有效

  •  1
  • Ryan Peschel  · 技术社区  · 5 年前

    我试图模仿其他移动聊天应用程序,当你选择 send-message 文本框,它打开虚拟键盘,最底部的消息仍然在视图中。令人惊讶的是,CSS似乎没有办法做到这一点,所以JavaScript resize (唯一能发现键盘何时打开和关闭的方法显然是)事件和手动滚动以进行救援。

    有人提供 this solution 我发现了 this solution 两者似乎都奏效了。

    除了一个案例。For 一些 原因,如果你在 MOBILE_KEYBOARD_HEIGHT (在我的例子中是250像素)像素的消息div底部,当你关闭移动键盘时,会发生一些奇怪的事情。使用前一种解决方案,它会滚动到底部。对于后一种解决方案,它反而会向上滚动 手机键盘重量 像素从底部。

    如果你滚动到这个高度以上,上面提供的两种解决方案都能完美地工作。只有当你接近底部时,他们才会有这个小问题。

    我想也许是我的程序导致了一些奇怪的杂散代码,但不,我甚至复制了一把小提琴,它确实有这个问题。很抱歉让调试变得如此困难,但如果你去 https://jsfiddle.net/t596hy8d/6/show (显示后缀提供全屏模式),您应该能够看到相同的行为。

    这种行为是,如果你向上滚动足够多,打开和关闭键盘会保持位置不变。然而,如果你 关闭 里面的键盘 手机键盘重量 在底部的像素处,你会发现它会滚动到底部。

    什么 导致这种情况的原因是什么?

    此处代码复制:

    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>
    0 回复  |  直到 5 年前
        1
  •  3
  •   halfer    5 年前

    我终于找到了一个解决方案 事实上 作品。虽然它可能并不理想,但它实际上在所有情况下都有效。以下是代码:

    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 活动。这似乎只发生在关闭键盘时,而不是打开键盘时。 这个 是您无法使用的原因 纸卷 要设置的事件 pxFromBottom ,因为如果你靠近底部,它会在 纸卷 事件发生前 调整大小 事件,打乱了计算。

    2. 所有解决方案在消息div底部附近都有困难的另一个原因有点难以理解。例如,在 my resize solution 我只是加上或减去250(移动键盘高度) scrollTop 当打开或关闭虚拟键盘时。除了靠近底部外,这工作得很好。为什么?因为假设你离底部50像素,然后关闭键盘。它将从中减去250 scrollTop (键盘高度),但应该只减去50!因此,当关闭底部附近的键盘时,它总是会重置到错误的固定位置。

    3. 我也相信你不能使用 onFocus onBlur 此解决方案的事件,因为这些事件仅在最初选择文本框打开键盘时发生。您完全可以在不激活这些事件的情况下打开和关闭移动键盘,因此,它们无法在此处使用。

    我认为上述几点对于开发解决方案很重要,因为它们起初并不明显,但会阻碍开发出一个强大的解决方案。

    我不喜欢这个解决方案(间隔有点低效,容易出现竞争情况),但我找不到更好的解决方案。

        2
  •  2
  •   Community CDub    5 年前

    我想你想要的是 overflow-anchor

    支持正在增加,但还不是全部 https://caniuse.com/#feat=css-overflow-anchor

    从a CSS-Tricks article on it:

    滚动锚定通过在当前位置上方的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 } 这将始终出现在这些新消息之后,因为新消息正在被插入 之前

    必须有一个滚动来注意锚定的变化,我认为这通常是好的用户体验。但无论哪种方式,当键盘打开时,应保持当前的滚动位置。

        3
  •  1
  •   Richard    5 年前

    我的解决方案与您提出的解决方案相同,但增加了条件检查。以下是对我的解决方案的描述:

    • 记录最后一个滚动位置 scrollTop 最后 clientHeight 属于 .messages oldScrollTop oldHeight 分别地
    • 更新 oldScrollTop 旧高度 每次a resize 发生在 window 并更新 oldScrollTop 每次a scroll 发生在 .messages
    • 什么时候? 窗口 缩小(当虚拟键盘显示时) .messages 将自动缩回。预期行为是使最底层的内容 .messages 即使在以下情况下仍然可见 .messages 高度回缩。这要求我们手动调整滚动位置 scrollTop 属于 .messages .
    • 当虚拟键盘显示时,更新 scrollTop 属于 .messages 以确保最底部的部分 .messages 在其高度收缩发生之前,仍然可以看到
    • 当虚拟键盘隐藏时,更新 scrollTop 属于 .messages 以确保最底部的部分 .messages 仍然是最底部的部分 .messages 高度膨胀后(除非膨胀不能向上发生;这发生在你几乎到达顶部时 .messages )

    是什么导致了这个问题?

    我(最初可能有缺陷)的逻辑思维是: 调整大小 发生, .messages '高度变化,更新于 .messages scrollTop 发生在我们的内部 调整大小 事件处理程序。然而,在 .messages 高度膨胀,a 纸卷 奇怪的是,事件发生在 调整大小 更奇怪的是 纸卷 事件 只有 当我们滚动到最大值以上时隐藏键盘时,就会发生这种情况 scrollTop when的价值 .messages 未收回。就我而言,这意味着当我向下滚动时 270.334px (最大值 scrollTop 之前 .messages 缩回)并隐藏键盘,这很奇怪 纸卷 之前 调整大小 事件发生并滚动您的 .messages 确切地说 270.334px 这显然打乱了我们上面的解决方案。

    幸运的是,我们可以解决这个问题。我个人对此的推断 纸卷 之前 调整大小 事件的发生是因为 .messages 无法维持其 scrollTop 上方位置 270.334px 当它在高度上膨胀时 (这就是为什么我提到我最初的逻辑思维是有缺陷的;仅仅是因为没有办法 .messages 保持其 scrollTop 高于其最大值的位置) 因此,它立即设置 scrollTop 达到它可以给出的最大值(毫不奇怪, 270.334px ).

    我们能做什么?

    因为我们只更新 旧高度 在调整大小时,我们可以检查这个强制滚动(或者更准确地说, 调整大小 )如果发生这种情况,请不要更新 oldScrollTop (因为我们已经处理过了 调整大小 !)我们只需要比较一下 旧高度 当前高度 纸卷 看看是否会发生这种强制滚动。这之所以有效,是因为 旧高度 不等于当前高度 纸卷 只有在以下情况下才是真的 调整大小 发生(这恰好是强制滚动发生的时候)。

    这是代码 (in 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移动版上进行了测试,它适用于这两种浏览器。