简体   繁体   English

如何防止文档滚动但允许在 iOS 和 Android 网站上的 div 元素内滚动?

[英]How to prevent document scrolling but allow scrolling inside div elements on websites for iOS and Android?

I created a website with jQueryMobile for iOS and Android.我使用 jQueryMobile 为 iOS 和 Android 创建了一个网站。

I don't want the document itself to scroll.我不希望文档本身滚动。 Instead, just an area (a <div> element) should be scrollable (via css property overflow-y:scroll ).相反,只有一个区域(一个<div>元素)应该是可滚动的(通过 css 属性overflow-y:scroll )。

So I disabled document scrolling via:所以我通过以下方式禁用了文档滚动:

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

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

But that will also disable scrolling for all other elements in the document, no matter if overflow:scroll is set or not.但这也将禁用文档中所有其他元素的滚动,无论是否设置了overflow:scroll

How can I solve this?我该如何解决这个问题?

How about this CSS only solution:这个仅 CSS 的解决方案怎么样:

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

body gets position: fixed; body获得position: fixed; and every other element you wish an overflow: scroll;以及您希望overflow: scroll;所有其他元素overflow: scroll; . . Works on mobile Chrome (WebKit)/Firefox 19/Opera 12.适用于移动 Chrome (WebKit)/Firefox 19/Opera 12。

You'll also see my various attempts towards a jQuery solution.您还将看到我对 jQuery 解决方案的各种尝试。 But as soon as you're binding touchmove / touchstart to document, it hinders scrolling in the child div no matter if unbinded or not.但是,一旦您将touchmove / touchstart绑定到文档,无论是否未绑定,它都会阻碍子 div 中的滚动。

Disclaimer: Solutions to this problem are in many ways basically not very nice UX-wise!免责声明:这个问题的解决方案在很多方面基本上都不是很好的用户体验! You'll never know how big the viewport of your visitors exactly is or which font-size they are using (client user-agent style like), therefore it could easily be, that important content is hidden to them in your document.您永远不会知道访问者的视口究竟有多大或他们使用的是哪种字体大小(类似于客户端用户代理样式),因此很容易在您的文档中对他们隐藏重要内容。

Finally, I got it to work.最后,我让它工作了。 Really simple:真的很简单:

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);
});

Maybe I misunderstood the question, but if I'm correct:也许我误解了这个问题,但如果我是对的:

You want not to be able to scroll except a certain element so you:您希望除某个元素外无法滚动,因此您:

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

Prevent everything within the document.防止文档中的所有内容。


Why don't you just stop the event bubbling on the element where you wish to scroll?为什么不停止在要滚动的元素上的事件冒泡? (PS: you don't have to prevent touchstart -> if you use touch start for selecting elements instead of clicks that is prevented as well, touch move is only needed because then it is actually tracing the movement) (PS:您不必阻止 touchstart -> 如果您使用 touch start 来选择元素而不是被阻止的点击,则只需要触摸移动,因为它实际上是在跟踪移动)

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

Now on the element CSS现在在元素 CSS 上

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

If you are on a mobile device, you can even go a step further.如果您使用的是移动设备,您甚至可以更进一步。 You can force hardware accelerated scrolling (though not all mobile browsers support this);您可以强制硬件加速滚动(尽管并非所有移动浏览器都支持);

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;
}

normal:普通的:

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

native feel:本土感觉:

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

Here is a solution I am using:这是我正在使用的解决方案:

$scrollElement is the scroll element, $scrollMask is a div with style position: fixed; top: 0; bottom: 0; $scrollElement 是滚动元素, $scrollMask 是一个样式为position: fixed; top: 0; bottom: 0;的 div position: fixed; top: 0; bottom: 0; position: fixed; top: 0; bottom: 0; . . The z-index of $scrollMask is smaller than $scrollElement. $scrollMask 的z-index小于 $scrollElement。

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

I was looking for a solution that did not require calling out specific areas that should scroll.我正在寻找一种不需要调出应该滚动的特定区域的解决方案。 Piecing together a few resources, here is what worked for me:拼凑一些资源,这对我有用:

    // 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;
            }
        }
    });

This code requires jQuery.此代码需要 jQuery。

Sources:资料来源:


Update更新

I needed a vanilla JavaScript version of this, so the following is a modified version.我需要一个普通的 JavaScript 版本,所以以下是修改后的版本。 I implemented a margin-checker and something that explicitly allows input/textareas to be clickable (I was running into issues with this on the project I used it on...it may not be necessary for your project).我实现了一个边距检查器和一些明确允许输入/文本区域可点击的东西(我在我使用它的项目中遇到了这个问题......你的项目可能不需要它)。 Keep in mind this is ES6 code.请记住,这是 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);

In my case, I have a scrollable body and a scrollable floating menu over it.就我而言,我有一个可滚动的主体和一个可滚动的浮动菜单。 Both have to be scrollable, but I had to prevent body scrolling when "floating menu" (position:fixed) received touch events and was scrolling and it reached top or bottom.两者都必须是可滚动的,但是当“浮动菜单”(位置:固定)接收到触摸事件并且正在滚动并到达顶部或底部时,我必须防止身体滚动。 By default browser then started to scroll the body.默认浏览器然后开始滚动正文。

I really liked jimmont's answer , but unfortunatelly it did not work well on all devices and browsers, especially with a fast and long swipe.我真的很喜欢jimmont 的回答,但不幸的是,它在所有设备和浏览器上都无法正常工作,尤其是在快速且长时间滑动的情况下。

I ended up using MOMENTUM SCROLLING USING JQUERY (hnldesign.nl) on floating menu, which prevents default browser scrolling and then animates scrolling itself.我最终在浮动菜单上使用MOMENTUM SCROLLING USING JQUERY (hnldesign.nl) ,这可以防止默认浏览器滚动,然后动画滚动本身。 I include that code here for completeness:为了完整起见我在此处包含该代码

/**
 * 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);
            }
        }
    });

Another observation: I also tried various combinations of e.stopPropagation() on menu div and e.preventDefault() on window/body at touchmove event, but without success, I only managed to prevent scrolling I wanted and not scrolling I did not want.另一个观察:我还在 touchmove 事件中尝试了菜单 div 上的 e.stopPropagation() 和 window/body 上的 e.preventDefault() 的各种组合,但没有成功,我只设法阻止了我想要的滚动,而不是我不想要的滚动. I also tried to have a div over whole document, with z-index between document and menu, visible only between touchstart and touchend, but it did not receive touchmove event (because it was under menu div).我还尝试在整个文档上设置一个 div,在文档和菜单之间使用 z-index,仅在 touchstart 和 touchend 之间可见,但它没有收到 touchmove 事件(因为它在菜单 div 下)。

Here is a solution that uses jQuery for the events.这是一个使用 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();
  };
});

First position the innerScroller wherever you want on the screen and then fix outerScroller by setting it css to 'hidden'.首先将innerScroller 放置在屏幕上您想要的任何位置,然后通过将css 设置为“隐藏”来修复outerScroller。 When you want to restore it you can set it back to 'auto' or 'scroll', whichever you used previously.当您想恢复它时,您可以将其设置回“自动”或“滚动”,无论您以前使用过哪个。

Here is my implementation which works on touch devices and laptops.这是我的实现,适用于触摸设备和笔记本电脑。

 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>

This is what worked for me for Android and IOS devices.这对我适用于 Android 和 IOS 设备。

Imagine that we have a div class="backdrop"> element that we don't want to be scrolled, ever.想象一下,我们有一个永远不想滚动的div class="backdrop">元素。 But we want to be able to scroll over an element that is on top of this backdrop .但是我们希望能够在这个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 });

So, we listen to the touchmove event, if we're scrolling over the backdrop, we prevent it.所以,我们监听touchmove事件,如果我们滚动背景,我们会阻止它。 If we're scrolling over something else, we allow it but stop its propagation so it doesn't scroll also the backdrop .如果我们正在滚动其他东西,我们会允许它但停止它的传播,因此它不会滚动backdrop

Of course this is pretty basic and can be re-worked and expanded a lot, but this is what fixed my issue in a VueJs2 project.当然,这是非常基本的,可以重新工作和扩展很多,但这是在 VueJs2 项目中解决我的问题的原因。

Hope it helps!希望能帮助到你! ;) ;)

I was able to disable scrolling of the main document by adding css "overflow-y: hidden" on HTML.我能够通过在 HTML 上添加 css "overflow-y: hidden" 来禁用主文档的滚动。

It did not mess with positioning at all.它根本没有搞乱定位。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM