Search code examples
javascripthtmlioscssmobile-safari

prevent full page scrolling iOS


Under Mobile Safari, is it possible to allow one absolutely positioned div to scroll without allowing the entire page to bob up and down when it the scroll reaches the edges (elastically scrolling)?

Here is a minimal working example of the issue I'm facing:

<!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        #a, #b {
            position: absolute;
            top: 0;
            left: 0;
            height: 100%;
            padding: 10px;
            overflow: auto;
        }
        #a {
            width: 80px;
            background: #f00;
        }
        #b {
            background: #00f;
            left: 80px;
            width: 100%;
        }
    </style>
    <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
    <script>
        function pdcb(e) {
            e.preventDefault();
        }
        function npcb(e) {
            e.stopPropagation();
        }
        $(document).on('touchstart touchmove', pdcb).
                    on('touchstart touchmove', '.scrollable', npcb);
    </script>
</head>
<body>
    <div id="a" class="scrollable">
        This<br>
        should<br>
        be<br>
        scrollable<br>
        but<br>
        not<br>
        scroll<br>
        the<br>
        whole<br>
        page<br>
        This<br>
        should<br>
        be<br>
        scrollable<br>
        but<br>
        not<br>
        scroll<br>
        the<br>
        whole<br>
        page<br>
        This<br>
        should<br>
        be<br>
        scrollable<br>
        but<br>
        not<br>
        scroll<br>
        the<br>
        whole<br>
        page<br>
        This<br>
        should<br>
        be<br>
        scrollable<br>
        but<br>
        not<br>
        scroll<br>
        the<br>
        whole<br>
        page<br>
        This<br>
        should<br>
        be<br>
        scrollable<br>
        but<br>
        not<br>
        scroll<br>
        the<br>
        whole<br>
        page<br>
    </div>
    <div id="b">
        this should never scroll
    </div>
</body>
</html>

Solution:

$(document).on('touchmove', function(e) {
    e.preventDefault();
}).ready(function() {
    $(".scrollable").on('touchstart', function(e) {
        this.allowUp = (this.scrollTop > 0);
        this.allowDown = (this.scrollTop < this.scrollHeight - this.clientHeight);
        this.prevTop = null;
        this.prevBot = null;
        this.lastY = e.originalEvent.pageY;
    }).on('touchmove', function(e) {
        var event = e.originalEvent;
        var up = (event.pageY > this.lastY), down = !up;
        this.lastY = event.pageY;

        if ((up && this.allowUp) || (down && this.allowDown))
            event.stopPropagation();
        else
            event.preventDefault();
    });
});

Solution

  • While you're not hitting the edges of your div's content, you need to allow the native touchmove event to work on that element (so it can scroll), but you're going to want to stop the event from bubbling up the DOM so that it doesn't trigger scrolling on the page body.

    When you hit the boundary of your element, you need to prevent the native momentum scrolling entirely.

    The code I use for this is as follows (apologies to the original author, this is adapted from a tutorial on this topic I found somewhere on the internet in the past... Can't seem to find the URL now though):

    where elem is your DOM node

    elem.addEventListener('touchstart', function(event){
        this.allowUp = (this.scrollTop > 0);
        this.allowDown = (this.scrollTop < this.scrollHeight - this.clientHeight);
        this.prevTop = null; this.prevBot = null;
        this.lastY = event.pageY;
    });
    
    elem.addEventListener('touchmove', function(event){
        var up = (event.pageY > this.lastY), down = !up;
        this.lastY = event.pageY;
    
        if ((up && this.allowUp) || (down && this.allowDown)) event.stopPropagation();
        else event.preventDefault();
    });
    

    I usually define an array of elements and loop through them - applying this code to each one iteratively.

    Best of luck, hope this helps.