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

后端 未结 11 1455
南笙
南笙 2020-12-04 10:43

I created a website with jQueryMobile for iOS and Android.

I don\'t want the document itself to scroll. Instead, just an area (a

element) sh
相关标签:
11条回答
  • 2020-12-04 11:14

    Here is a solution that uses jQuery for the events.

    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();
      };
    });
    
    0 讨论(0)
  • 2020-12-04 11:17

    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)

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

    Now on the element 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>
    
    0 讨论(0)
  • 2020-12-04 11:18

    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.

    I ended up using MOMENTUM SCROLLING USING JQUERY (hnldesign.nl) on floating menu, which prevents default browser scrolling and then animates scrolling itself. 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. 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).

    0 讨论(0)
  • 2020-12-04 11:19

    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>

    0 讨论(0)
  • 2020-12-04 11:26

    I was able to disable scrolling of the main document by adding css "overflow-y: hidden" on HTML.

    It did not mess with positioning at all.

    0 讨论(0)
  • 2020-12-04 11:27

    Here is a solution I am using:

    $scrollElement is the scroll element, $scrollMask is a div with style position: fixed; top: 0; bottom: 0;. The z-index of $scrollMask is smaller than $scrollElement.

    $scrollElement.on('touchmove touchstart', function (e) {
        e.stopPropagation();
    });
    
    $scrollMask.on('touchmove', function(e) {
        e.stopPropagation();
        e.preventDefault();
    });
    
    0 讨论(0)
提交回复
热议问题