【问题标题】:How to prevent document scrolling but allow scrolling inside div elements on websites for iOS and Android?如何防止文档滚动但允许在 iOS 和 Android 网站上的 div 元素内滚动?
【发布时间】:2013-02-14 11:20:21
【问题描述】:

我使用 jQueryMobile 为 iOS 和 Android 创建了一个网站。

我不希望文档本身滚动。相反,只有一个区域(<div> 元素)应该是可滚动的(通过 css 属性 overflow-y:scroll)。

所以我通过以下方式禁用了文档滚动:

$(document).bind("touchstart", function(e){
    e.preventDefault();
});

$(document).bind("touchmove", function(e){
    e.preventDefault();
});

但这也会禁用文档中所有其他元素的滚动,无论是否设置了overflow:scroll

我该如何解决这个问题?

【问题讨论】:

    标签: javascript html css jquery-mobile scroll


    【解决方案1】:

    这个CSS only解决方案怎么样:

    https://jsfiddle.net/Volker_E/jwGBy/24/

    body 获取 position: fixed; 以及您希望的所有其他元素 overflow: scroll;。 适用于移动 Chrome (WebKit)/Firefox 19/Opera 12。

    您还将看到我对 jQuery 解决方案的各种尝试。但是,一旦您将touchmove/touchstart 绑定到文档,无论是否未绑定,都会阻碍在子 div 中滚动。

    免责声明:这个问题的解决方案在很多方面基本上都不是很好的用户体验!您永远不会知道访问者的视口到底有多大或他们使用的字体大小(如客户端用户代理样式),因此很容易将重要内容隐藏在您的文档中。

    【讨论】:

    • 嗯,你的想法不错,但不幸的是在<body> 上使用position:fixed 完全破坏了我依赖<body> 的布局是静态的:-(
    • 嘿,问题是这样的:我有一个网站,其中<body> 中有一个<div> 元素。这个 div 有 overflow-y:auto;-webkit-overflow-scrolling:touch 集。当其内容溢出时,用户可以在其中滚动。但是,当您向下滚动到最底部或最顶部并从屏幕上松开手指然后再次滚动时,它将滚动 div 之外的文档(因为已到达 div 的末尾)而不是 div本身。我不想那样。因此,我只想关闭文档滚动。
    • 也许:stackoverflow.com/questions/10238084/… 对您有进一步的帮助?
    • @Volker E. 这个解决方案如此简单,我很惊讶!我使用了隐藏在身体上的溢出,但那是相似的,很棒的工作
    • 迄今为止我找到的最简单的解决方案,在我的设置中就像一个魅力。我一直在用 JS preventDefault 和其他选项来解决这个问题,但这很好用。谢谢
    【解决方案2】:

    也许我误解了这个问题,但如果我是正确的:

    您希望除了某个元素之外不能滚动,因此您:

    $(document).bind("touchmove", function(e){
        e.preventDefault();
    });
    

    阻止文档中的所有内容。


    你为什么不停止你想要滚动的元素上的事件冒泡呢? (PS:您不必阻止 touchstart -> 如果您使用触摸开始来选择元素而不是也阻止单击,则仅需要触摸移动,因为它实际上是在跟踪移动)

    $('#element').on('touchmove', function (e) {
         e.stopPropagation();
    });
    

    现在在元素 CSS 上

    #element {
       overflow-y: scroll; // (vertical) 
       overflow-x: hidden; // (horizontal)
    }
    

    如果您使用的是移动设备,您甚至可以更进一步。 您可以强制硬件加速滚动(尽管并非所有移动浏览器都支持此功能);

    Browser Overflow scroll:
    
    Android Browser Yes
    Blackberry Browser  Yes
    Chrome for Mobile   Yes
    Firefox Mobile  Yes
    IE Mobile           Yes
    Opera Mini          No
    Opera Mobile    Kinda
    Safari          Yes
    
    #element.nativescroll {
        -webkit-overflow-scrolling: touch;
    }
    

    正常:

    <div id="element"></div>
    

    原生感觉:

    <div id="element" class="nativescroll"></div>
    

    【讨论】:

    • 这个解决方案对任何人都有效吗?不适合我。一旦顶层到达其滚动的末端,滚动就会继续到底层(覆盖层之后)。
    • 啊,这已经接近完美的解决方案了。适用于除我的手机之外的所有设备。我可以看到我想要滚动的元素的滚动条,但是当我滑动时没有任何反应。
    • 这绝对是完美的。 @Ben 对顶层到达末尾后底层滚动的担忧对我来说不是问题。在我的情况下,底层将反弹回其正常位置。我只想阻止正常滚动,因为它是一个网络画布,但如果滚动确实发生,它会自行修复,不会影响体验
    • 非常感谢,你用这个拯救了我的一天:-webkit-overflow-scrolling: touch
    【解决方案3】:

    最后,我得到了它的工作。真的很简单:

    var $layer = $("#layer");
    $layer.bind('touchstart', function (ev) {
        var $this = $(this);
        var layer = $layer.get(0);
    
        if ($this.scrollTop() === 0) $this.scrollTop(1);
        var scrollTop = layer.scrollTop;
        var scrollHeight = layer.scrollHeight;
        var offsetHeight = layer.offsetHeight;
        var contentHeight = scrollHeight - offsetHeight;
        if (contentHeight == scrollTop) $this.scrollTop(scrollTop-1);
    });
    

    【讨论】:

    • 你能帮我更好地理解你的代码吗? #theDiv 和“滚动条”是什么意思?
    • @relima 对不起,我的回答有误。 theDiv 和 scroller 实际上是同一个对象。这是应该滚动的 div 容器。我在回答中更正了这一点。
    • 这是我见过的最好和最简单的解决方案,我已经研究这个问题好几天了。但是,此脚本仅在您位于可滚动 div 的顶部时才阻止文档滚动,而不是在您滚动 div 的底部时阻止文档滚动。这适用于 iOS7!
    • 您可能希望将 document.getElementById('scroller') 粘贴到变量中,而不是连续调用 3 次。
    • 非常好的答案。这是一种防止浏览器滚动模式以外的内容的好方法。
    【解决方案4】:

    这是我正在使用的解决方案:

    $scrollElement 是滚动元素,$scrollMask 是一个样式为position: fixed; top: 0; bottom: 0; 的div。 $scrollMask 的z-index 小于$scrollElement。

    $scrollElement.on('touchmove touchstart', function (e) {
        e.stopPropagation();
    });
    
    $scrollMask.on('touchmove', function(e) {
        e.stopPropagation();
        e.preventDefault();
    });
    

    【讨论】:

    • 你能详细说明这个例子吗,也许是一个jsfiddle?我不确定我是否了解掩码和元素之间的关系或如何使用它们。
    【解决方案5】:

    我正在寻找一种不需要调用应滚动的特定区域的解决方案。拼凑一些资源,这对我有用:

        // Detects if element has scroll bar
        $.fn.hasScrollBar = function() {
            return this.get(0).scrollHeight > this.outerHeight();
        }
    
        $(document).on("touchstart", function(e) {
            var $scroller;
            var $target = $(e.target);
    
            // Get which element could have scroll bars
            if($target.hasScrollBar()) {
                $scroller = $target;
            } else {
                $scroller = $target
                    .parents()
                    .filter(function() {
                        return $(this).hasScrollBar();
                    })
                    .first()
                ;
            }
    
            // Prevent if nothing is scrollable
            if(!$scroller.length) {
                e.preventDefault();
            } else {
                var top = $scroller[0].scrollTop;
                var totalScroll = $scroller[0].scrollHeight;
                var currentScroll = top + $scroller[0].offsetHeight;
    
                // If at container edge, add a pixel to prevent outer scrolling
                if(top === 0) {
                    $scroller[0].scrollTop = 1;
                } else if(currentScroll === totalScroll) {
                    $scroller[0].scrollTop = top - 1;
                }
            }
        });
    

    此代码需要 jQuery。

    来源:


    更新

    我需要一个原生的 JavaScript 版本,所以以下是修改后的版本。我实现了一个边距检查器和一些明确允许输入/文本区域可点击的东西(我在我使用它的项目中遇到了这个问题......你的项目可能不需要它)。请记住,这是 ES6 代码。

    const preventScrolling = e => {
        const shouldAllowEvent = element => {
            // Must be an element that is not the document or body
            if(!element || element === document || element === document.body) {
                return false;
            }
    
            // Allow any input or textfield events
            if(['INPUT', 'TEXTAREA'].indexOf(element.tagName) !== -1) {
                return true;
            }
    
            // Get margin and outerHeight for final check
            const styles = window.getComputedStyle(element);
            const margin = parseFloat(styles['marginTop']) +
                parseFloat(styles['marginBottom']);
            const outerHeight = Math.ceil(element.offsetHeight + margin);
    
            return (element.scrollHeight > outerHeight) && (margin >= 0);
        };
    
        let target = e.target;
    
        // Get first element to allow event or stop
        while(target !== null) {
            if(shouldAllowEvent(target)) {
                break;
            }
    
            target = target.parentNode;
        }
    
        // Prevent if no elements
        if(!target) {
            e.preventDefault();
        } else {
            const top = target.scrollTop;
            const totalScroll = target.scrollHeight;
            const currentScroll = top + target.offsetHeight;
    
            // If at container edge, add a pixel to prevent outer scrolling
            if(top === 0) {
                target.scrollTop = 1;
            } else if(currentScroll === totalScroll) {
                target.scrollTop = top - 1;
            }
        }
    };
    
    document.addEventListener('touchstart', preventScrolling);
    document.addEventListener('mousedown', preventScrolling);
    

    【讨论】:

      【解决方案6】:

      就我而言,我有一个可滚动的主体和一个可滚动的浮动菜单。两者都必须是可滚动的,但是当“浮动菜单”(位置:固定)接收到触摸事件并且正在滚动并到达顶部或底部时,我必须阻止正文滚动。默认情况下,浏览器开始滚动正文。

      我真的很喜欢jimmont's answer,但不幸的是,它在所有设备和浏览器上都无法正常工作,尤其是在快速长滑时。

      我最终在浮动菜单上使用了MOMENTUM SCROLLING USING JQUERY (hnldesign.nl),它阻止了默认浏览器滚动,然后动画滚动本身。为了完整起见,我在此处包含that code

      /**
       * jQuery inertial Scroller v1.5
       * (c)2013 hnldesign.nl
       * This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/.
       **/
      /*jslint browser: true*/
      /*global $, jQuery*/
      
      /* SETTINGS */
      var i_v = {
          i_touchlistener     : '.inertialScroll',         // element to monitor for touches, set to null to use document. Otherwise use quotes. Eg. '.myElement'. Note: if the finger leaves this listener while still touching, movement is stopped.
          i_scrollElement     : '.inertialScroll',         // element (class) to be scrolled on touch movement
          i_duration          : window.innerHeight * 1.5, // (ms) duration of the inertial scrolling simulation. Devices with larger screens take longer durations (phone vs tablet is around 500ms vs 1500ms). This is a fixed value and does not influence speed and amount of momentum.
          i_speedLimit        : 1.2,                      // set maximum speed. Higher values will allow faster scroll (which comes down to a bigger offset for the duration of the momentum scroll) note: touch motion determines actual speed, this is just a limit.
          i_handleY           : true,                     // should scroller handle vertical movement on element?
          i_handleX           : true,                     // should scroller handle horizontal movement on element?
          i_moveThreshold     : 100,                      // (ms) determines if a swipe occurred: time between last updated movement @ touchmove and time @ touchend, if smaller than this value, trigger inertial scrolling
          i_offsetThreshold   : 30,                       // (px) determines, together with i_offsetThreshold if a swipe occurred: if calculated offset is above this threshold
          i_startThreshold    : 5,                        // (px) how many pixels finger needs to move before a direction (horizontal or vertical) is chosen. This will make the direction detection more accurate, but can introduce a delay when starting the swipe if set too high
          i_acceleration      : 0.5,                      // increase the multiplier by this value, each time the user swipes again when still scrolling. The multiplier is used to multiply the offset. Set to 0 to disable.
          i_accelerationT     : 250                       // (ms) time between successive swipes that determines if the multiplier is increased (if lower than this value)
      };
      /* stop editing here */
      
      //set some required vars
      i_v.i_time  = {};
      i_v.i_elem  = null;
      i_v.i_elemH = null;
      i_v.i_elemW = null;
      i_v.multiplier = 1;
      
      // Define easing function. This is based on a quartic 'out' curve. You can generate your own at http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
      if ($.easing.hnlinertial === undefined) {
          $.easing.hnlinertial = function (x, t, b, c, d) {
              "use strict";
              var ts = (t /= d) * t, tc = ts * t;
              return b + c * (-1 * ts * ts + 4 * tc + -6 * ts + 4 * t);
          };
      }
      
      $(i_v.i_touchlistener || document)
          .on('touchstart touchmove touchend', function (e) {
              "use strict";
              //prevent default scrolling
              e.preventDefault();
              //store timeStamp for this event
              i_v.i_time[e.type]  = e.timeStamp;
          })
          .on('touchstart', function (e) {
              "use strict";
              this.tarElem = $(e.target);
              this.elemNew = this.tarElem.closest(i_v.i_scrollElement).length > 0 ? this.tarElem.closest(i_v.i_scrollElement) : $(i_v.i_scrollElement).eq(0);
              //dupecheck, optimizes code a bit for when the element selected is still the same as last time
              this.sameElement = i_v.i_elem ? i_v.i_elem[0] == this.elemNew[0] : false;
              //no need to redo these if element is unchanged
              if (!this.sameElement) {
                  //set the element to scroll
                  i_v.i_elem = this.elemNew;
                  //get dimensions
                  i_v.i_elemH = i_v.i_elem.innerHeight();
                  i_v.i_elemW = i_v.i_elem.innerWidth();
                  //check element for applicable overflows and reevaluate settings
                  this.i_scrollableY      = !!((i_v.i_elemH < i_v.i_elem.prop('scrollHeight') && i_v.i_handleY));
                  this.i_scrollableX    = !!((i_v.i_elemW < i_v.i_elem.prop('scrollWidth') && i_v.i_handleX));
              }
              //get coordinates of touch event
              this.pageY      = e.originalEvent.touches[0].pageY;
              this.pageX      = e.originalEvent.touches[0].pageX;
              if (i_v.i_elem.is(':animated') && (i_v.i_time.touchstart - i_v.i_time.touchend) < i_v.i_accelerationT) {
                  //user swiped while still animating, increase the multiplier for the offset
                  i_v.multiplier += i_v.i_acceleration;
              } else {
                  //else reset multiplier
                  i_v.multiplier = 1;
              }
              i_v.i_elem
                  //stop any animations still running on element (this enables 'tap to stop')
                  .stop(true, false)
                  //store current scroll positions of element
                  .data('scrollTop', i_v.i_elem.scrollTop())
                  .data('scrollLeft', i_v.i_elem.scrollLeft());
          })
          .on('touchmove', function (e) {
              "use strict";
              //check if startThreshold is met
              this.go = (Math.abs(this.pageX - e.originalEvent.touches[0].pageX) > i_v.i_startThreshold || Math.abs(this.pageY - e.originalEvent.touches[0].pageY) > i_v.i_startThreshold);
          })
          .on('touchmove touchend', function (e) {
              "use strict";
              //check if startThreshold is met
              if (this.go) {
                  //set animpar1 to be array
                  this.animPar1 = {};
                  //handle events
                  switch (e.type) {
                  case 'touchmove':
                      this.vertical       = Math.abs(this.pageX - e.originalEvent.touches[0].pageX) < Math.abs(this.pageY - e.originalEvent.touches[0].pageY); //find out in which direction we are scrolling
                      this.distance       = this.vertical ? this.pageY - e.originalEvent.touches[0].pageY : this.pageX - e.originalEvent.touches[0].pageX; //determine distance between touches
                      this.acc            = Math.abs(this.distance / (i_v.i_time.touchmove - i_v.i_time.touchstart)); //calculate acceleration during movement (crucial)
                      //determine which property to animate, reset animProp first for when no criteria is matched
                      this.animProp       = null;
                      if (this.vertical && this.i_scrollableY) { this.animProp = 'scrollTop'; } else if (!this.vertical && this.i_scrollableX) { this.animProp = 'scrollLeft'; }
                      //set animation parameters
                      if (this.animProp) { this.animPar1[this.animProp] = i_v.i_elem.data(this.animProp) + this.distance; }
                      this.animPar2       = { duration: 0 };
                      break;
                  case 'touchend':
                      this.touchTime      = i_v.i_time.touchend - i_v.i_time.touchmove; //calculate touchtime: the time between release and last movement
                      this.i_maxOffset    = (this.vertical ? i_v.i_elemH : i_v.i_elemW) * i_v.i_speedLimit; //(re)calculate max offset
                      //calculate the offset (the extra pixels for the momentum effect
                      this.offset         = Math.pow(this.acc, 2) * (this.vertical ? i_v.i_elemH : i_v.i_elemW);
                      this.offset         = (this.offset > this.i_maxOffset) ? this.i_maxOffset : this.offset;
                      this.offset         = (this.distance < 0) ? -i_v.multiplier * this.offset : i_v.multiplier * this.offset;
                      //if the touchtime is low enough, the offset is not null and the offset is above the offsetThreshold, (re)set the animation parameters to include momentum
                      if ((this.touchTime < i_v.i_moveThreshold) && this.offset !== 0 && Math.abs(this.offset) > (i_v.i_offsetThreshold)) {
                          if (this.animProp) { this.animPar1[this.animProp] = i_v.i_elem.data(this.animProp) + this.distance + this.offset; }
                          this.animPar2   = { duration: i_v.i_duration, easing : 'hnlinertial', complete: function () {
                              //reset multiplier
                              i_v.multiplier = 1;
                          }};
                      }
                      break;
                  }
      
                  // run the animation on the element
                  if ((this.i_scrollableY || this.i_scrollableX) && this.animProp) {
                      i_v.i_elem.stop(true, false).animate(this.animPar1, this.animPar2);
                  }
              }
          });
      

      另一个观察结果:我还在 touchmove 事件中尝试了菜单 div 上的 e.stopPropagation() 和窗口/主体上的 e.preventDefault() 的各种组合,但没有成功,我只设法阻止我想要的滚动而不是滚动我不想要。我还尝试在整个文档上设置一个 div,在文档和菜单之间使用 z-index,仅在 touchstart 和 touchend 之间可见,但它没有收到 touchmove 事件(因为它在菜单 div 下)。

      【讨论】:

        【解决方案7】:

        这是一个使用 jQuery 处理事件的解决方案。

        var stuff = {};
        $('#scroller').on('touchstart',stuff,function(e){
          e.data.max = this.scrollHeight - this.offsetHeight;
          e.data.y = e.originalEvent.pageY;
        }).on('touchmove',stuff,function(e){
          var dy = e.data.y - e.originalEvent.pageY;
          // if scrolling up and at the top, or down and at the bottom
          if((dy < 0 && this.scrollTop < 1)||(dy > 0 && this.scrollTop >= e.data.max)){
            e.preventDefault();
          };
        });
        

        【讨论】:

          【解决方案8】:

          首先将 innerScroller 放置在屏幕上您想要的任何位置,然后通过将其 css 设置为“隐藏”来修复 outerScroller。当您想恢复它时,您可以将其设置回“自动”或“滚动”,无论您以前使用过哪个。

          【讨论】:

            【解决方案9】:

            这是我的实现,适用于触摸设备和笔记本电脑。

            function ScrollManager() {
                let startYCoord;
            
                function getScrollDiff(event) {
                    let delta = 0;
            
                    switch (event.type) {
                        case 'mousewheel':
                            delta = event.wheelDelta ? event.wheelDelta : -1 * event.deltaY;
                            break;
                        case 'touchstart':
                            startYCoord = event.touches[0].clientY;
                            break;
                        case 'touchmove': {
                            const yCoord = event.touches[0].clientY;
            
                            delta = yCoord - startYCoord;
                            startYCoord = yCoord;
                            break;
                        }
                    }
            
                    return delta;
                }
            
                function getScrollDirection(event) {
                    return getScrollDiff(event) >= 0 ? 'UP' : 'DOWN';
                }
            
                function blockScrollOutside(targetElement, event) {
                    const { target } = event;
                    const isScrollAllowed = targetElement.contains(target);
                    const isTouchStart = event.type === 'touchstart';
            
                    let doScrollBlock = !isTouchStart;
            
                    if (isScrollAllowed) {
                        const isScrollingUp = getScrollDirection(event) === 'UP';
                        const elementHeight = targetElement.scrollHeight - targetElement.offsetHeight;
            
                        doScrollBlock =
                            doScrollBlock &&
                            ((isScrollingUp && targetElement.scrollTop <= 0) ||
                                (!isScrollingUp && targetElement.scrollTop >= elementHeight));
                    }
            
                    if (doScrollBlock) {
                        event.preventDefault();
                    }
                }
            
                return {
                    blockScrollOutside,
                    getScrollDirection,
                };
            }
            
            const scrollManager = ScrollManager();
            const testBlock = document.body.querySelector('.test');
            
            function handleScroll(event) {
              scrollManager.blockScrollOutside(testBlock, event);
            }
            
            window.addEventListener('scroll', handleScroll);
            window.addEventListener('mousewheel', handleScroll);
            window.addEventListener('touchstart', handleScroll);
            window.addEventListener('touchmove', handleScroll);
            .main {
               border: 1px solid red;
               height: 200vh;
             }
             
             .test {
               border: 1px solid green;
               height: 300px;
               width: 300px;
               overflow-y: auto;
               position: absolute;
               top: 100px;
               left: 50%;
             }
             
             .content {
               height: 100vh;
             }
            <div class="main">
              <div class="test">
                <div class="content"></div>
              </div>
            </div>

            【讨论】:

              【解决方案10】:

              这对我来说适用于 Android 和 IOS 设备。

              想象一下,我们有一个永远不想滚动的div class="backdrop"&gt; 元素。但我们希望能够滚动到位于此 backdrop 之上的元素。

              function handleTouchMove(event) {
                  const [backdrop] = document.getElementsByClassName('backdrop');
                  const isScrollingBackdrop = backdrop === event.target;
              
                  isScrollingBackdrop ? event.preventDefault() : event.stopPropagation();
              }
              
              window.addEventListener('touchmove', handleTouchMove, { passive: false });
              

              所以,我们监听touchmove 事件,如果我们滚动背景,我们会阻止它。如果我们正在滚动其他内容,我们允许它但停止它的传播,因此它不会滚动backdrop

              当然,这是非常基本的,可以重新设计和扩展很多,但这是解决我在 VueJs2 项目中的问题的原因。

              希望对您有所帮助! ;)

              【讨论】:

                【解决方案11】:

                我可以通过在 HTML 上添加 css "overflow-y: hidden" 来禁用主文档的滚动。

                它根本没有弄乱定位。

                【讨论】:

                  猜你喜欢
                  • 2012-01-07
                  • 2021-08-15
                  • 1970-01-01
                  • 2012-03-06
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-04-25
                  • 1970-01-01
                  • 2011-11-27
                  相关资源
                  最近更新 更多