ChatGPT解决这个技术问题 Extra ChatGPT

iOS Safari – How to disable overscroll but allow scrollable divs to scroll normally?

I'm working on an iPad-based web app, and need to prevent overscrolling so that it seems less like a web page. I'm currently using this to freeze the viewport and disable overscroll:

document.body.addEventListener('touchmove',function(e){
      e.preventDefault();
  });

This works great to disable overscroll but my app has several scrollable divs, and the above code prevents them from scrolling.

I'm targeting iOS 5 and above only so I've avoided hacky solutions like iScroll. Instead I'm using this CSS for my scrollable divs:

.scrollable {
    -webkit-overflow-scrolling: touch;
    overflow-y:auto;
}

This works without the document overscroll script, but doesn't solve the div scrolling problem.

Without a jQuery plugin, is there any way to use the overscroll fix but exempt my $('.scrollable') divs?

EDIT:

I found something that's a decent solution:

 // Disable overscroll / viewport moving on everything but scrollable divs
 $('body').on('touchmove', function (e) {
         if (!$('.scrollable').has($(e.target)).length) e.preventDefault();
 });

The viewport still moves when you scroll past the beginning or end of the div. I'd like to find a way to disable that as well.

tried your final one also but didnt work either
I was able keep the viewport from moving when you scroll past the end of the div by explicitly capturing the scroll event on the parent of the scrollable div and not allowing it to actually scroll. If you're using jquery mobile it makes sense to do this at the page level like so: $('div[data-role="page"]').on('scroll', function(e) {e.preventDefault(); });
I have found this script which fix this problem! :) github.com/lazd/iNoBounce
Why would you post the link again if someone above your post, posted it 7 months earlier?

C
Community

This solves the issue when you scroll past the beginning or end of the div

var selScrollable = '.scrollable';
// Uses document because document will be topmost level in bubbling
$(document).on('touchmove',function(e){
  e.preventDefault();
});
// Uses body because jQuery on events are called off of the element they are
// added to, so bubbling would not work if we used document instead.
$('body').on('touchstart', selScrollable, function(e) {
  if (e.currentTarget.scrollTop === 0) {
    e.currentTarget.scrollTop = 1;
  } else if (e.currentTarget.scrollHeight === e.currentTarget.scrollTop + e.currentTarget.offsetHeight) {
    e.currentTarget.scrollTop -= 1;
  }
});
// Stops preventDefault from being called on document if it sees a scrollable div
$('body').on('touchmove', selScrollable, function(e) {
  e.stopPropagation();
});

Note that this won't work if you want to block whole page scrolling when a div does not have overflow. To block that, use the following event handler instead of the one immediately above (adapted from this question):

$('body').on('touchmove', selScrollable, function(e) {
    // Only block default if internal div contents are large enough to scroll
    // Warning: scrollHeight support is not universal. (https://stackoverflow.com/a/15033226/40352)
    if($(this)[0].scrollHeight > $(this).innerHeight()) {
        e.stopPropagation();
    }
});

This won't work if there is an iframe inside the scrollable area and the user starts scrolling on that iframe. Is there a workaround for this?
Worked great - this is definitely better than just targeting .scrollable directly (which is what I had originally tried in solving this problem). If you're a JavaScript noob and want easy code to remove these handlers somewhere down the line, these two lines work great for me! $(document).off('touchmove'); AND $('body').off('touchmove touchstart', '.scrollable');
It worked perfectly for me. Thanks a lot, you saved me hours!
This doesn't work if there's not enough content in the div to scroll. Someone made a separate question that answered that here: stackoverflow.com/q/16437182/40352
How can I allow more than one ".scrollable" class? it works fine with one but I need to make scrollable another div as well. Thanks!
C
Community

Using Tyler Dodge's excellent answer kept lagging on my iPad, so I added some throttling code, now it's quite smooth. There is some minimal skipping sometimes while scrolling.

// Uses document because document will be topmost level in bubbling
$(document).on('touchmove',function(e){
  e.preventDefault();
});

var scrolling = false;

// Uses body because jquery on events are called off of the element they are
// added to, so bubbling would not work if we used document instead.
$('body').on('touchstart','.scrollable',function(e) {

    // Only execute the below code once at a time
    if (!scrolling) {
        scrolling = true;   
        if (e.currentTarget.scrollTop === 0) {
          e.currentTarget.scrollTop = 1;
        } else if (e.currentTarget.scrollHeight === e.currentTarget.scrollTop + e.currentTarget.offsetHeight) {
          e.currentTarget.scrollTop -= 1;
        }
        scrolling = false;
    }
});

// Prevents preventDefault from being called on document if it sees a scrollable div
$('body').on('touchmove','.scrollable',function(e) {
  e.stopPropagation();
});

Also, adding the following CSS fixes some rendering glitches (source):

.scrollable {
    overflow: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
}
.scrollable * {
    -webkit-transform: translate3d(0,0,0);
}

This won't work if there is an iframe inside the scrollable area and the user starts scrolling on that iframe. Is there a workaround for this?
Seems to work perfect for dragging backwards, but dragging downwards will still move safari.
An awesome solution... Thanks a lot :)
This worked for me. Thanks! I spend to more than 1.5 days to solve this issue.
This is awesome, worked great and saved me further stress trying to work out a solution. Thank you Kuba!
J
Jonathan Tonge

First prevent default actions on your entire document as usual:

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

Then stop your class of elements from propagating to the document level. This stops it from reaching the function above and thus e.preventDefault() is not initiated:

$('.scrollable').bind('touchmove', function(e){
  e.stopPropagation();
});

This system seems to be more natural and less intensive than calculating the class on all touch moves. Use .on() rather than .bind() for dynamically generated elements.

Also consider these meta tags to prevent unfortunate things from happening while using your scrollable div:

<meta content='True' name='HandheldFriendly' />
<meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0' name='viewport' />
<meta name="viewport" content="width=device-width" />

D
DeveloperJoe

Can you just add a little more logic into your overscroll disabling code to make sure the targeted element in question is not one that you would like to scroll? Something like this:

document.body.addEventListener('touchmove',function(e){
     if(!$(e.target).hasClass("scrollable")) {
       e.preventDefault();
     }
 });

Thanks... It seems like this should work, but it doesn't. Also, shouldn't it be "scrollable" and not ".scrollable" (with the dot)?
It seems like it is the most deeply nested element that receives the touch event so you might need to check all of your parents to see if you are in a scrollable div.
Why would one use document.body.addEventListener if jQuery is used? Is that for a reason?
E
Elias Fyksen

Best solution to this is css/html: Make a div to wrap your elements in, if you dont have it already And set it to position fixed and overflow hidden. Optional, set height and width to 100% if you want it to fill the whole screen and nothing but the whole screen

#wrapper{ height: 100%; width: 100%; position: fixed; overflow: hidden; }

All

Your

Elements


J
Johan

Check if the scrollable element is already scrolled to the top when trying to scroll up or to the bottom when trying to scroll down and then preventing the default action to stop the entire page from moving.

var touchStartEvent;
$('.scrollable').on({
    touchstart: function(e) {
        touchStartEvent = e;
    },
    touchmove: function(e) {
        if ((e.originalEvent.pageY > touchStartEvent.originalEvent.pageY && this.scrollTop == 0) ||
            (e.originalEvent.pageY < touchStartEvent.originalEvent.pageY && this.scrollTop + this.offsetHeight >= this.scrollHeight))
            e.preventDefault();
    }
});

I had to check for e.originalEvent.touches[0].pageY instead of e.originalEvent.pageY. It worked but only if you are already at the end of the scrolling div. When the scroll is in progress (eg. you have scrolled really fast) it does not stop once the end of the scrollable div is reached.
n
nrutas

I was looking for a way to prevent all body scrolling when there's a popup with a scrollable area (a "shopping cart" popdown that has a scrollable view of your cart).

I wrote a far more elegant solution using minimal javascript to just toggle the class "noscroll" on your body when you have a popup or div that you'd like to scroll (and not "overscroll" the whole page body).

while desktop browsers observe overflow:hidden -- iOS seems to ignore that unless you set the position to fixed... which causes the whole page to be a strange width, so you have to set the position and width manually as well. use this css:

.noscroll {
    overflow: hidden;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
}

and this jquery:

/* fade in/out cart popup, add/remove .noscroll from body */
$('a.cart').click(function() {
    $('nav > ul.cart').fadeToggle(100, 'linear');
    if ($('nav > ul.cart').is(":visible")) {
        $('body').toggleClass('noscroll');
    } else {
        $('body').removeClass('noscroll');
    }
});

/* close all popup menus when you click the page... */
$('body').click(function () {
    $('nav > ul').fadeOut(100, 'linear');
    $('body').removeClass('noscroll');
});

/* ... but prevent clicks in the popup from closing the popup */
$('nav > ul').click(function(event){
    event.stopPropagation();
});

This is very helpful, and a minimal approach, just what I needed. Setting the position to fixed, with top:0; left:0; width:100%; were the elements I was missing. This is also useful for fly-out menus.
P
PiñaDesign

I've work a little workarround without jquery. Not perfert but works fine (especially if you have a scroll-x in a scoll-y) https://github.com/pinadesign/overscroll/

Fell free to participate and improve it


Had the same issue as Jeff, tried every answer, yours worked. Thank you!
The accepted answer only worked for me when the div with .scrollable had enough content to cause it to overflow. If it didn't overflow, the 'bounce' effect still existed. However this works perfectly, thanks!
j
jcbdrn

This solution doesn't require you to put a scrollable class on all your scrollable divs so is more general. Scrolling is allowed on all elements which are, or are children of, INPUT elements contenteditables and overflow scroll or autos.

I use a custom selector and I also cache the result of the check in the element to improve performance. No need to check the same element every time. This may have a few issues as only just written but thought I'd share.

$.expr[':'].scrollable = function(obj) {
    var $el = $(obj);
    var tagName = $el.prop("tagName");
    return (tagName !== 'BODY' && tagName !== 'HTML') && (tagName === 'INPUT' || $el.is("[contentEditable='true']") || $el.css("overflow").match(/auto|scroll/));
};
function preventBodyScroll() {
    function isScrollAllowed($target) {
        if ($target.data("isScrollAllowed") !== undefined) {
            return $target.data("isScrollAllowed");
        }
        var scrollAllowed = $target.closest(":scrollable").length > 0;
        $target.data("isScrollAllowed",scrollAllowed);
        return scrollAllowed;
    }
    $('body').bind('touchmove', function (ev) {
        if (!isScrollAllowed($(ev.target))) {
            ev.preventDefault();
        }
    });
}

N
Nicolas Bouvrette

While disabling all "touchmove" events might seem like a good idea, as soon as you need other scrollable elements on the page it will cause problems. On top of that, if you only disable "touchmove" events on certain elements (e.g. body if you want the page to be non-scrollable), as soon as it is enabled anywhere else, IOS will cause unstoppable propagation in Chrome when the URL bar toggles.

While I cannot explain this behavior, it looks like the only way to prevent seems to set the body's position to fixed. The only problem doing is that you will lose the position of the document - this is especially annoying in modals for example. One way to solve it would be to use these simple VanillaJS functions:

function disableDocumentScrolling() {
    if (document.documentElement.style.position != 'fixed') {
        // Get the top vertical offset.
        var topVerticalOffset = (typeof window.pageYOffset != 'undefined') ?
            window.pageYOffset : (document.documentElement.scrollTop ? 
            document.documentElement.scrollTop : 0);
        // Set the document to fixed position (this is the only way around IOS' overscroll "feature").
        document.documentElement.style.position = 'fixed';
        // Set back the offset position by user negative margin on the fixed document.
        document.documentElement.style.marginTop = '-' + topVerticalOffset + 'px';
    }
}

function enableDocumentScrolling() {
    if (document.documentElement.style.position == 'fixed') {
        // Remove the fixed position on the document.
        document.documentElement.style.position = null;
        // Calculate back the original position of the non-fixed document.
        var scrollPosition = -1 * parseFloat(document.documentElement.style.marginTop);
        // Remove fixed document negative margin.
        document.documentElement.style.marginTop = null;
        // Scroll to the original position of the non-fixed document.
        window.scrollTo(0, scrollPosition);
    }
}

Using this solution you can have a fixed document and any other element in your page can overflow by using simple CSS (e.g., overflow: scroll;). No need for special classes or anything else.


C
Christopher Johnson

Here's a zepto compatible solution

    if (!$(e.target).hasClass('scrollable') && !$(e.target).closest('.scrollable').length > 0) {
       console.log('prevented scroll');
       e.preventDefault();
       window.scroll(0,0);
       return false;
    }

R
RJS

this one works for me (plain javascript)

var fixScroll = function (className, border) {  // className = class of scrollElement(s), border: borderTop + borderBottom, due to offsetHeight
var reg = new RegExp(className,"i"); var off = +border + 1;
function _testClass(e) { var o = e.target; while (!reg.test(o.className)) if (!o || o==document) return false; else o = o.parentNode; return o;}
document.ontouchmove  = function(e) { var o = _testClass(e); if (o) { e.stopPropagation(); if (o.scrollTop == 0) { o.scrollTop += 1; e.preventDefault();}}}
document.ontouchstart = function(e) { var o = _testClass(e); if (o && o.scrollHeight >= o.scrollTop + o.offsetHeight - off) o.scrollTop -= off;}
}

fixScroll("fixscroll",2); // assuming I have a 1px border in my DIV

html:

<div class="fixscroll" style="border:1px gray solid">content</div>

F
Frits

Try this It'll work perfect.

$('body.overflow-hidden').delegate('#skrollr-body','touchmove',function(e){
    e.preventDefault();
    console.log('Stop skrollrbody');
}).delegate('.mfp-auto-cursor .mfp-content','touchmove',function(e){
    e.stopPropagation();
    console.log('Scroll scroll');
});

L
Láďa Durchánek

I had surprising luck with with simple:

body {
    height: 100vh;
}

It works great to disable overscroll for pop-ups or menus and it doesn't force browser bars to appear like when using position:fixed. BUT - you need to save scroll position before setting fixed height and restore it when hiding the pop-up, otherwise, browser will scroll to top.


b
bagli
overscroll-behavior: none;

overscroll

The overscroll-behavior property is a new CSS feature that controls the behavior of what happens when you over-scroll a container (including the page itself). You can use it to cancel scroll chaining, disable/customize the pull-to-refresh action, disable rubberbanding effects on iOS (when Safari implements overscroll-behavior), and more. The best part is that using overscroll-behavior does not adversely affect page performance.


A good answer will always include an explanation why this would solve the issue, so that the OP and any future readers can learn from it.
@Tyler2P I edited my answer, hope its okay now, thanks for suggestion :)