¿Cómo evitar el desplazamiento de documentos pero permitir el desplazamiento dentro de elementos div en sitios web para iOS y Android?


He creado un sitio web con jQueryMobile para iOS y Android.

No quiero que el documento en sí se desplace. En su lugar, solo un área (un elemento <div>) debe ser desplazable (a través de la propiedad css overflow-y:scroll).

Así que deshabilité el desplazamiento del documento a través de:

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

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

Pero eso también deshabilitará el desplazamiento para todos los demás elementos del documento, sin importar si overflow:scroll está configurado o no.

¿Cómo puedo resolver esto?

Author: Timo, 2013-02-28

9 answers

¿Qué tal esta solución CSS only :

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

body obtiene position: fixed; y todos los demás elementos que desee un overflow: scroll;. Funciona en Chrome móvil (WebKit)/Firefox 19 / Opera 12.

También verá mis varios intentos hacia una solución jQuery. Pero tan pronto como usted está vinculante touchmove/touchstart para documentar, dificulta el desplazamiento en el div hijo, sin importar si no está vinculado o no.

Descargo de responsabilidad: Las soluciones a este problema están en muchos formas básicamente no muy agradable UX-sabio! Nunca sabrá qué tan grande es exactamente la ventana de visualización de sus visitantes o qué tamaño de fuente están utilizando (estilo cliente user-agent), por lo tanto, podría ser fácilmente que el contenido importante esté oculto para ellos en su documento.

 25
Author: Volker E.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-02 15:46:36

Finalmente, conseguí que funcionara. Muy 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);
});
 11
Author: Timo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-23 08:52:09

Tal vez entendí mal la pregunta, pero si estoy en lo correcto:

Usted no quiere ser capaz de desplazarse excepto un determinado elemento por lo que:

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

Evitar todo dentro del documento.


¿Por qué no dejas que el evento burbujee en el elemento donde deseas desplazarte? (PD: no tiene que evitar touchstart - > si usa touch start para seleccionar elementos en lugar de clics que también se evitan, touch move solo es necesario porque entonces en realidad es rastreando el movimiento)

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

Ahora en el elemento CSS

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

Si está en un dispositivo móvil, incluso puede ir un paso más allá. Puede forzar el desplazamiento acelerado por hardware (aunque no todos los navegadores móviles admiten esto);

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>

Sentimiento nativo:

<div id="element" class="nativescroll"></div>
 10
Author: Danillo Felixdaal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-17 20:11:16

Estaba buscando una solución que no requería llamar a áreas específicas que deberían desplazarse. Juntando algunos recursos, esto es lo que funcionó para mí:

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

Este código requiere jQuery.

Fuentes:


Actualización

Necesitaba una versión JavaScript de vainilla de esto, por lo que la siguiente es una versión modificada. Implementé un verificador de márgenes y algo que explícitamente permite que se pueda hacer clic en las áreas de entrada/texto (me estaba topando con problemas con esto en el proyecto que lo usé on...it puede no ser necesario para su proyecto). Tenga en cuenta que este es el código 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);
 3
Author: David Sinclair,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:35

Aquí hay una solución que estoy usando:

{scrollElement es el elemento scroll,, scrollMask es un div con estilo position: fixed; top: 0; bottom: 0;. El z-index deask scrollMask es más pequeño que $scrollElement.

$scrollElement.on('touchmove touchstart', function (e) {
    e.stopPropagation();
});
$scrollMask.on('touchmove', function(e) {
    e.stopPropagation();
    e.preventDefault();
});
 2
Author: John Xiao,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-13 07:44:47

En mi caso, tengo un cuerpo desplazable y un menú flotante desplazable sobre él. Ambos tienen que ser desplazables, pero tuve que evitar el desplazamiento del cuerpo cuando "menú flotante" (posición:fijo) recibió eventos táctiles y se desplazaba y llegaba a la parte superior o inferior. Por defecto, el navegador comenzó a desplazarse por el cuerpo.

Me gustó mucho la respuesta de jimmont, pero desafortunadamente no funcionó bien en todos los dispositivos y navegadores, especialmente con un golpe rápido y largo.

Terminé usando DESPLAZAMIENTO DE MOMENTUM USANDO JQUERY (hnldesign.nl) en el menú flotante, que evita el desplazamiento predeterminado del navegador y luego anima el desplazamiento. Incluyo ese código aquí para completar:

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

Otra observación: También probé varias combinaciones de e.stopPropagation() en menu div y e.preventDefault() en window/body en el evento touchmove, pero sin éxito, solo logré evitar el desplazamiento que quería y no el desplazamiento que no quería. También traté de tener un div sobre todo el documento, con z-index entre el documento y el menú, visible solo entre touchstart y touchend, pero no recibió el evento touchmove (porque estaba bajo menu div).

 2
Author: Miha Pirnat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:11

Aquí hay una solución que utiliza jQuery para los eventos.

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
Author: jimmont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-31 19:19:56

Primero coloque el innerScroller donde quiera en la pantalla y luego arregle outerScroller configurándolo css a 'hidden'. Cuando desee restaurarlo, puede volver a configurarlo en 'auto' o 'scroll', lo que haya utilizado anteriormente.

 0
Author: Seraj Ahmad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-27 18:13:18

Aquí está mi implementación que funciona en dispositivos táctiles y portátiles.

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
Author: Yekver,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-28 16:32:53