iOS 10 Safari:防止在固定覆盖层后面滚动并保持滚动位置

Posted

技术标签:

【中文标题】iOS 10 Safari:防止在固定覆盖层后面滚动并保持滚动位置【英文标题】:iOS 10 Safari: Prevent scrolling behind a fixed overlay and maintain scroll position 【发布时间】:2017-05-26 11:41:33 【问题描述】:

在显示固定位置叠加层时,我无法阻止主体内容滚动。类似的问题已经被问过很多次了,但所有以前有效的技术似乎都不适用于 ios 10 中的 Safari。这似乎是最近的问题。

一些注意事项:

如果我将htmlbody 都设置为overflow: hidden,我可以禁用滚动,但这会使正文内容滚动到顶部。 如果覆盖中的内容足够长以至于可以滚动,则可以正确禁用主页内容的滚动。如果叠加层中的内容不够长导致滚动,您可以滚动主页内容。 我包含了一个来自https://blog.christoffer.online/2015-06-10-six-things-i-learnt-about-ios-rubberband-overflow-scrolling/ 的javascript 函数,它在显示覆盖时禁用touchmove。这以前有效,但不再有效。

这是完整的 HTML 源代码:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
    <style type="text/css">
        html, body 
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        
        body 
            font-family: arial;
        
        #overlay 
            display: none;
            position: fixed;
            z-index: 9999;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            overflow: scroll;
            color: #fff;
            background: rgba(0, 0, 0, 0.5);
        
        #overlay span 
            position: absolute;
            display: block;
            right: 10px;
            top: 10px;
            font-weight: bold;
            font-size: 44px;
            cursor: pointer;
        
        #overlay p 
            display: block;
            padding: 100px;
            font-size: 36px;
        
        #page 
            width: 100%;
            height: 100%;
        
        a 
            font-weight: bold;
            color: blue;
        
    </style>
    <script>
        $(function() 
            $('a').click(function(e) 
                e.preventDefault();
                $('body').css('overflow', 'hidden');
                $('#page').addClass('disable-scrolling'); // for touchmove technique below

                $('#overlay').fadeIn();
            );
            $('#overlay span').click(function() 
                $('body').css('overflow', 'auto');
                $('#page').removeClass('disable-scrolling'); // for touchmove technique below

                $('#overlay').fadeOut();
            );
        );

        /* Technique from http://blog.christoffer.me/six-things-i-learnt-about-ios-safaris-rubber-band-scrolling/ */
        document.ontouchmove = function ( event ) 
            var isTouchMoveAllowed = true, target = event.target;
            while ( target !== null ) 
                if ( target.classList && target.classList.contains( 'disable-scrolling' ) ) 
                    isTouchMoveAllowed = false;
                    break;
                
                target = target.parentNode;
            
            if ( !isTouchMoveAllowed ) 
                event.preventDefault();
            
        ;
    </script>
</head>

<body>
    <div id="overlay">
        <span>&times;</span>
        <p>fixed popover</p>
    </div>

    <div id="page">
        <strong>this is the top</strong><br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        <br>
        <div><a href="#">Show Popover</a></div>
        <br>
        <br>

    </div>

</body>

</html>

【问题讨论】:

【参考方案1】:

-webkit-overflow-scrolling: touch; 添加到#overlay 元素。

然后将这段 JavaScript 代码添加到 body 标记的末尾:

(function () 
  var _overlay = document.getElementById('overlay');
  var _clientY = null; // remember Y position on touch start

  _overlay.addEventListener('touchstart', function (event) 
    if (event.targetTouches.length === 1) 
      // detect single touch
      _clientY = event.targetTouches[0].clientY;
    
  , false);

  _overlay.addEventListener('touchmove', function (event) 
    if (event.targetTouches.length === 1) 
      // detect single touch
      disableRubberBand(event);
    
  , false);

  function disableRubberBand(event) 
    var clientY = event.targetTouches[0].clientY - _clientY;

    if (_overlay.scrollTop === 0 && clientY > 0) 
      // element is at the top of its scroll
      event.preventDefault();
    

    if (isOverlayTotallyScrolled() && clientY < 0) 
      //element is at the top of its scroll
      event.preventDefault();
    
  

  function isOverlayTotallyScrolled() 
    // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
    return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight;
  
())

【讨论】:

代码最好保存在外部文件中,以便缓存。 我想同样的技巧适用于似乎有同样问题的 Firefox for iOS? 我必须复制所有 js 才能在覆盖毯位于顶部时禁用后台滚动?希望有更优雅的解决方案 vue mixin火热发货! gist.github.com/vovchisko/7222b0270a5953a9074abb5876720a7a 如果需要阻止动力,我只是在下面添加了自己的解决方案,受@BohdanDidukh 的影响:) -- ***.com/a/57566116/2321594【参考方案2】:

将 Bohdan Didukh 的方法与我之前的方法相结合,创建了一个易于使用的 npm 包来禁用/启用正文滚动。

https://github.com/willmcpo/body-scroll-lock

有关该解决方案如何工作的更多详细信息,请阅读https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177

【讨论】:

是的,不幸的是,这不适用于移动 Safari。我还没有找到任何真正的修复移动 safari 滚动的方法。 这个包阻止固定位置模态在我的情况下在 IOS 设备上滚动。 这个包裹对我来说是金子。很棒的工作@Will!【参考方案3】:

长期以来,我一直试图找到一个干净的解决方案,似乎对我最有效的是在正文上设置pointer-events: none;,然后在我想要允许滚动的项目上明确设置pointer-events: auto;在。

【讨论】:

感谢您的提示。我试试看。 在iOS 12上试过,没有效果:/ @t0vana 我们在 ios 12 的生产环境中使用它,它对我们有用。 我能够让它工作的唯一方法是为所有设置为position: absolute; width: 100%; height: 100%; overflow: scroll; 的网站内容设置一个包装器元素,并在该包装器元素上设置pointer-events: none。将其设置为 body 无效。我认为这是最好的方法,因为 Apple 似乎一直在打破任何其他技术。 啊,谢谢!这对我来说适用于移动 safari。如果我将pointer-events: none 添加到正文和包装元素,然后将pointer-events: auto 添加到我要滚动的元素【参考方案4】:

Bohdan 上面的solution 很棒。然而,它不会捕捉/阻止势头——即当用户不在页面的确切顶部,而是在页面顶部附近的情况(例如,scrollTop 为 5px ) 突然间,用户突然大规模下拉! Bohand 的解决方案捕获了touchmove 事件,但由于-webkit-overflow-scrolling is momentum based,动量本身会导致额外的滚动,在我的情况下隐藏标题并且真的很烦人。

为什么会这样?

其实-webkit-overflow-scrolling: touch是一个双重用途的属性。

    很好的目的是提供rubberband smooth scrolling effect,这在iOS 设备上的自定义overflow:scrolling 容器元素中几乎是必需的。 然而,不想要的目的是这种“过度滚动”。这是有道理的,因为这一切都是为了平稳而不是突然停止! :)

动量阻塞解决方案

我为自己提出的解决方案改编自 Bohdan 的解决方案,但我没有阻止 touchmove 事件,而是更改了上述 CSS 属性。

只需在挂载/渲染时将具有overflow: scroll(和-webkit-overflow-scrolling: touch)的元素传递给此函数。

这个函数的返回值应该在destroy/beforeDestroy的时候调用。

const disableOverscroll = function(el: HTMLElement) 
    function _onScroll() 
        const isOverscroll = (el.scrollTop < 0) || (el.scrollTop > el.scrollHeight - el.clientHeight);
        el.style.webkitOverflowScrolling = (isOverscroll) ? "auto" : "touch";
        //or we could have: el.style.overflow = (isOverscroll) ? "hidden" : "auto";
    

    function _listen() 
        el.addEventListener("scroll", _onScroll, true);
    

    function _unlisten() 
        el.removeEventListener("scroll", _onScroll);
    

    _listen();
    return _unlisten();

快速解决方案

或者,如果您不关心unlistening(不建议这样做),更简短的答案是:

el = document.getElementById("overlay");
el.addEventListener("scroll", function 
    const isOverscroll = (el.scrollTop < 0) || (el.scrollTop > el.scrollHeight - el.clientHeight);
    el.style.webkitOverflowScrolling = (isOverscroll) ? "auto" : "touch";
, true);

【讨论】:

【参考方案5】:

只需更改 body 上的溢出滚动行为对我有用:

body 
    -webkit-overflow-scrolling: touch;

【讨论】:

不错。我没想到。【参考方案6】:

我在 safari(iOS 版)上也遇到了同样的问题。我首先考虑了解决方案。但我不相信这些黑客。然后我了解财产touch-action。添加touch-action: none 覆盖为我解决了这个问题。对于上面的问题,将touch-action:none 添加到覆盖范围内。

  #overlay span 
        position: absolute;
        display: block;
        right: 10px;
        top: 10px;
        font-weight: bold;
        font-size: 44px;
        cursor: pointer;
        touch-action: none;
    

【讨论】:

这是 safari > 13 的正确答案。不幸的是,并非所有苹果设备都已更新。但touch-action 结合body, html position: fixed; 似乎现在可以工作。【参考方案7】:

当您的叠加层打开时,您可以将 prevent-scroll 之类的类添加到 body 以防止滚动叠加层后面的元素:

body.prevent-scroll 
  position: fixed;
  overflow: hidden;
  width: 100%;
  height: 100%;

https://codepen.io/claudiojs/pen/ZKeLvq

【讨论】:

恐怕不会保持滚动位置。【参考方案8】:

对于那些使用 React 的人,我已经成功地将 @bohdan-didukh 的解决方案放入组件的 componentDidMount 方法中。像这样的东西(可以通过移动浏览器查看链接):

class Hello extends React.Component 
  componentDidMount = () => 
    var _overlay = document.getElementById('overlay');
    var _clientY = null; // remember Y position on touch start

    function isOverlayTotallyScrolled() 
        // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
        return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight;
    

    function disableRubberBand(event) 
        var clientY = event.targetTouches[0].clientY - _clientY;

        if (_overlay.scrollTop === 0 && clientY > 0) 
            // element is at the top of its scroll
            event.preventDefault();
        

        if (isOverlayTotallyScrolled() && clientY < 0) 
            //element is at the top of its scroll
            event.preventDefault();
        
    

    _overlay.addEventListener('touchstart', function (event) 
        if (event.targetTouches.length === 1) 
            // detect single touch
            _clientY = event.targetTouches[0].clientY;
        
    , false);

    _overlay.addEventListener('touchmove', function (event) 
        if (event.targetTouches.length === 1) 
            // detect single touch
            disableRubberBand(event);
        
    , false);
  

  render() 
    // border and padding just to illustrate outer scrolling disabled 
    // when scrolling in overlay, and enabled when scrolling in outer
    // area
    return <div style= border: "1px solid red", padding: "48px" >
      <div id='overlay' style= border: "1px solid black", overflowScrolling: 'touch', WebkitOverflowScrolling: 'touch' >
        [...Array(10).keys()].map(x => <p>Text</p>)
      </div>
    </div>;
  


ReactDOM.render(
  <Hello name="World" />,
  document.getElementById('container')
);

可通过手机查看:https://jsbin.com/wiholabuka

可编辑链接:https://jsbin.com/wiholabuka/edit?html,js,output

【讨论】:

【参考方案9】:

在某些正文内容隐藏在叠加层后面的情况下,您可以使用const scrollPos = window.scrollY 存储当前滚动位置,然后将position: fixed; 应用于正文。当模型关闭时,从 body 中移除固定位置并运行 window.scrollTo(0, scrollPos) 以恢复之前的位置。

这对我来说是最简单的解决方案,代码量最少。

【讨论】:

这对我来说也是最简单的解决方案,因为如果有嵌套的滚动容器,其他解决方案会变得非常复杂。【参考方案10】:

我在github 上找到了代码。它适用于 iOS 10、11、12 中的 Safari

/* ScrollClass */
class ScrollClass 
constructor () 
    this.$body = $('body');

    this.styles = 
        disabled: 
            'height': '100%',
            'overflow': 'hidden',
        ,

        enabled: 
            'height': '',
            'overflow': '',
        
    ;


disable ($element = $(window)) 
    let disabled = false;
    let scrollTop = window.pageYOffset;

    $element
        .on('scroll.disablescroll', (event) => 
            event.preventDefault();

            this.$body.css(this.styles.disabled);

            window.scrollTo(0, scrollTop);
            return false;
        )
        .on('touchstart.disablescroll', () => 
            disabled = true;
        )
        .on('touchmove.disablescroll', (event) => 
            if (disabled) 
                event.preventDefault();
            
        )
        .on('touchend.disablescroll', () => 
            disabled = false;
        );


enable ($element = $(window)) 
    $element.off('.disablescroll');

    this.$body.css(this.styles.enabled);


使用:

Scroll = new ScrollClass();

Scroll.disable();// disable scroll for $(window)

Scroll.disable($('element'));// disable scroll for $('element')

Scroll.enable();// enable scroll for $(window)

Scroll.enable($('element'));// enable scroll for $('element')

希望对你有帮助。

【讨论】:

【参考方案11】:

只需设置bodyoverflow: hidden可以在modal popups时禁止body滚动。并设置bodyoverflow: auto使其可以再次滚动。

function lockBodyScroll(lock) 
  if (lock) 
    $('body').css('overflow', 'hidden');
   else 
    $('body').css('overflow', 'auto');
  

【讨论】:

在使用固定位置叠加时在 iOS 上不起作用。

以上是关于iOS 10 Safari:防止在固定覆盖层后面滚动并保持滚动位置的主要内容,如果未能解决你的问题,请参考以下文章

iOS safari 允许通过固定 div 滚动 body

位置:当显示标签栏时,固定“hitboxes”在大尺寸手机上的 iOS 10 Safari 中向上移动

如何防止滚动条覆盖 IE10 中的内容?

固定定位在 Safari 7 中不起作用

防止Safari上的半透明URL和状态栏

iOS 上的 Safari 仅在滚动停止后显示固定元素