Búsqueda de sitios web

javascript - ¿Cómo evitar que el elemento padre se desplace cuando la posición de desplazamiento del elemento interno alcanza la parte superior/inferior?


Tengo una pequeña "caja de herramientas flotante" - un div con position:fixed; overflow:auto. Funciona perfectamente.

Pero al desplazarse dentro de ese cuadro (con la rueda del mouse) y llegar al fondo O al principio, el elemento principal "se hace cargo" de la "solicitud de desplazamiento": el documento detrás de la caja de herramientas se desplaza.
- Lo cual es molesto y no es lo que el usuario "pide".

Estoy usando jQuery y pensé que podría detener este comportamiento con event.stoppropagation():
$ ("#toolBox").scroll( function(event){ event.stoppropagation() });

Sí entra en la función, pero aun así, la propagación ocurre de todos modos (el documento se desplaza)
- Es sorprendentemente difícil buscar este tema en SO (y Google), así que tengo que preguntar:
¿Cómo prevenir la propagación/burbujeo del evento de desplazamiento?

Editar:
Solución funcional gracias a amustill (y a Brandon Aaron por el plugin de rueda del ratón aquí:
https://github.com/brandonaaron/jquery-mousewheel/raw/master/jquery.mousewheel.js

$(".ToolPage").bind('mousewheel', function(e, d)  
    var t = $(this);
    if (d > 0 && t.scrollTop() === 0) {
        e.preventDefault();
    }
    else {
        if (d < 0 && (t.scrollTop() == t.get(0).scrollHeight - t.innerHeight())) {
            e.preventDefault();
        }
    }
});

Respuesta #1

Estoy agregando esta respuesta por completitud porque la respuesta aceptada por @amustill no resuelve correctamente el problema en Internet Explorer. Por favor, consulte los comentarios en mi publicación original para más detalles. Además, esta solución no requiere ningún complemento, solo jQuery.

En esencia, el código funciona manejando el evento mousewheel. Cada uno de estos eventos contiene un wheelDelta igual al número de px que va a mover el área desplazable. Si este valor es >0, entonces estamos desplazándonos hacia arriba. Si el wheelDelta es <0, entonces estamos desplazándonos hacia abajo.

FireFox: FireFox usa DOMMouseScroll como el evento, y llena originalEvent.detail, cuyo +/- está invertido respecto a lo descrito arriba. Generalmente devuelve intervalos de 3, mientras que otros navegadores devuelven desplazamiento en intervalos de 120 (al menos en mi máquina). Para corregirlo, simplemente lo detectamos y multiplicamos por -40 para normalizar.

La respuesta de @amustill funciona cancelando el evento si el área desplazable del <div> ya está en la posición máxima superior o inferior. Sin embargo, Internet Explorer ignora el evento cancelado en situaciones donde el delta es mayor que el espacio desplazable restante.

En otras palabras, si tienes un <div> de 200px de alto que contiene 500px de contenido desplazable, y el scrollTop actual es 400, un evento mousewheel que le indique al navegador desplazarse 120px más hará que tanto el <div> como el <body> se desplacen, porque 400 + 120 > 500.

Entonces, para resolver el problema, tenemos que hacer algo ligeramente diferente, como se muestra a continuación:

El código jQuery requerido es:

$(document).on('DOMMouseScroll mousewheel', '.Scrollable', function(ev) {
    var $this = $(this),
        scrollTop = this.scrollTop,
        scrollHeight = this.scrollHeight,
        height = $this.innerHeight(),
        delta = (ev.type == 'DOMMouseScroll' ?
            ev.originalEvent.detail * -40 :
            ev.originalEvent.wheelDelta),
        up = delta > 0;

    var prevent = function() {
        ev.stopPropagation();
        ev.preventDefault();
        ev.returnValue = false;
        return false;
    }

    if (!up && -delta > scrollHeight - height - scrollTop) {
        // Scrolling down, but this will take us past the bottom.
        $this.scrollTop(scrollHeight);
        return prevent();
    } else if (up && delta > scrollTop) {
        // Scrolling up, but this will take us past the top.
        $this.scrollTop(0);
        return prevent();
    }
});

En esencia, este código cancela cualquier evento de desplazamiento que crearía la condición de borde no deseada, luego usa jQuery para establecer el scrollTop del <div> al valor máximo o mínimo, dependiendo de la dirección que el evento mousewheel estaba solicitando.

Debido a que el evento se cancela completamente en cualquier caso, nunca se propaga al body en absoluto y, por lo tanto, resuelve el problema en IE, así como en todos los demás navegadores.

También he puesto un ejemplo funcional en jsFiddle.

Respuesta #2

TL;DR: Puedes usar overscroll-behavior: contain de manera segura para lograr esto en navegadores modernos.


Todas las soluciones dadas en este hilo no mencionan una forma existente - y nativa - de resolver este problema sin reordenar el DOM y/o usar trucos para prevenir eventos. Pero hay una buena razón: esta forma es propietaria - y está disponible solo en la plataforma web de MS. Citando a MSDN:

Propiedad -ms-scroll-chaining - especifica el comportamiento de desplazamiento que ocurre cuando un usuario alcanza el límite de desplazamiento durante una manipulación. Valores de propiedad:

encadenado - Valor inicial. El elemento padre desplazable más cercano comienza a desplazarse cuando el usuario alcanza un límite de desplazamiento durante una manipulación. No se muestra efecto de rebote.

ninguno - Se muestra un efecto de rebote cuando el usuario alcanza un límite de desplazamiento durante una manipulación.

Cierto, esta propiedad solo es compatible con IE10+/Edge. Aún así, aquí hay una cita reveladora:

Para darte una idea de lo popular que puede ser prevenir el encadenamiento de desplazamiento, según mi rápida búsqueda en http-archive, "-ms-scroll-chaining: none" se usa en el 0,4 % de las 300.000 páginas principales a pesar de tener una funcionalidad limitada y solo ser compatible con IE/Edge.

¡Y ahora buenas noticias, todos! A partir de Chrome 63, finalmente tenemos una solución nativa para las plataformas basadas en Blink también, y eso incluye tanto Chrome (obviamente) como Android WebView (pronto).

Citando el artículo introductorio:

La propiedad overscroll-behavior es una nueva característica de CSS que controla el comportamiento de lo que sucede cuando se hace un sobre-desplazamiento en un contenedor (incluida la propia página). Se puede usar para cancelar la encadenación de desplazamiento, deshabilitar/personalizar la acción de tirar para actualizar, deshabilitar efectos de rebote en iOS (cuando Safari implemente overscroll-behavior) y más.[...]

La propiedad toma tres valores posibles:

auto - Predeterminado. Los desplazamientos que se originan en el elemento pueden propagarse a elementos ancestros.

contain - previene la cadena de desplazamiento. Los desplazamientos no se propagan a los elementos ascendentes, pero se muestran los efectos locales dentro del nodo. Por ejemplo, el efecto de resplandor al sobrepasar el desplazamiento en Android o el efecto de rebote en iOS, que notifica al usuario cuando ha alcanzado un límite de desplazamiento. Nota: usar overscroll-behavior: contain en el elemento html evita las acciones de navegación por sobrepasar el desplazamiento.

none - igual que contain pero también previene los efectos de sobre desplazamiento dentro del nodo mismo (por ejemplo, el resplandor de sobre desplazamiento de Android o el efecto de goma de iOS).

[...] ¡La mejor parte es que usar overscroll-behavior no afecta negativamente el rendimiento de la página como los trucos mencionados en la introducción!

Aquí está esta funcionalidad en acción. Y aquí está el correspondiente documento del módulo CSS.

ACTUALIZACIÓN: Firefox, desde la versión 59, se ha unido al club, y se espera que MS Edge implemente esta función en la versión 18. Aquí está el correspondiente caniusage.

ACTUALIZACIÓN 2: Y ahora (octubre de 2022) Safari se unió oficialmente al club: desde la versión 16.0, overscroll-behavior ya no está detrás de la bandera de función.

Respuesta #3

Es posible con el uso del complemento Mousewheel de Brandon Aaron.

Aquí hay una demostración: http://jsbin.com/jivutakama/edit?html,js,output

$(function() {

  var toolbox = $('#toolbox'),
      height = toolbox.height(),
      scrollHeight = toolbox.get(0).scrollHeight;

  toolbox.bind('mousewheel', function(e, d) {
    if((this.scrollTop === (scrollHeight - height) && d < 0) || (this.scrollTop === 0 && d > 0)) {
      e.preventDefault();
    }
  });

});

Respuesta #4

Sé que es una pregunta bastante antigua, pero dado que esta es una de las principales resultados en Google... Tenía que cancelar de alguna manera la propagación del desplazamiento sin jQuery y este código funciona para mí:

function preventDefault(e) {
  e = e || window.event;
  if (e.preventDefault)
    e.preventDefault();
  e.returnValue = false;  
}

document.getElementById('a').onmousewheel = function(e) { 
  document.getElementById('a').scrollTop -= e. wheelDeltaY; 
  preventDefault(e);
}

Respuesta #5

EDITAR: ejemplo de CodePen

Para AngularJS, definí la siguiente directiva:

module.directive('isolateScrolling', function () {
  return {
    restrict: 'A',
      link: function (scope, element, attr) {
        element.bind('DOMMouseScroll', function (e) {
          if (e.detail > 0 && this.clientHeight + this.scrollTop == this.scrollHeight) {
            this.scrollTop = this.scrollHeight - this.clientHeight;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }
          else if (e.detail < 0 && this.scrollTop <= 0) {
            this.scrollTop = 0;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }
        });
        element.bind('mousewheel', function (e) {
          if (e.deltaY > 0 && this.clientHeight + this.scrollTop >= this.scrollHeight) {
            this.scrollTop = this.scrollHeight - this.clientHeight;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }
          else if (e.deltaY < 0 && this.scrollTop <= 0) {
            this.scrollTop = 0;
            e.stopPropagation();
            e.preventDefault();
            return false;
          }

          return true;
        });
      }
  };
});

Y luego lo añadió al elemento desplazable (el ul del menú desplegable):

<div class="dropdown">
  <button type="button" class="btn dropdown-toggle">Rename <span class="caret"></span></button>
  <ul class="dropdown-menu" isolate-scrolling>
    <li ng-repeat="s in savedSettings | objectToArray | orderBy:'name' track by s.name">
      <a ng-click="renameSettings(s.name)">{{s.name}}</a>
    </li>
  </ul>
</div>

Probado en Chrome y Firefox. El desplazamiento suave de Chrome derrota este truco cuando se realiza un gran movimiento de la rueda del ratón cerca (pero no en) la parte superior o inferior de la región de desplazamiento.

Respuesta #6

Hay toneladas de preguntas como esta por ahí, con muchas respuestas, pero no pude encontrar una solución satisfactoria que no involucrara eventos, scripts, plugins, etc. Quería mantenerlo directamente en HTML y CSS. Finalmente encontré una solución que funcionó, aunque implicaba reestructurar el marcado para romper la cadena de eventos.

1. Problema básico

El desplazamiento de entrada (es decir: la rueda del ratón) aplicado al elemento modal se desbordará en un elemento ancestro y lo desplazará en la misma dirección, si algún elemento de este tipo es desplazable:

(Todos los ejemplos están destinados a visualizarse en resoluciones de escritorio)

https://jsfiddle.net/ybkbg26c/5/

HTML :

<div id="parent">
  <div id="modal">
    This text is pretty long here.  Hope fully, we will get some scroll bars.
  </div>
</div>

CSS :

#modal {
  position: absolute;
  height: 100px;
  width: 100px;
  top: 20%;
  left: 20%;
  overflow-y: scroll;
}
#parent {
  height: 4000px;
}

2. No desplazar el contenedor padre al desplazar el modal

La razón por la que el ancestro termina desplazándose es porque el evento de desplazamiento hace burbuja y algún elemento en la cadena puede manejarlo. Una manera de detener eso es asegurarse de que ninguno de los elementos en la cadena sepa cómo manejar el desplazamiento. En términos de nuestro ejemplo, podemos refactorizar el árbol para mover el modal fuera del elemento padre. Por razones poco claras, no es suficiente mantener al padre y al modal como elementos DOM hermanos; el padre debe estar envuelto por otro elemento que establezca un nuevo contexto de apilamiento. Un contenedor con posición absoluta alrededor del padre puede hacer el truco.

El resultado que obtenemos es que mientras el modal reciba el evento de desplazamiento, el evento no se propagará al elemento "padre".

Normalmente debería ser posible rediseñar el árbol DOM para soportar este comportamiento sin afectar lo que ve el usuario final.

https://jsfiddle.net/0bqq31Lv/3/

HTML :

<div id="context">
  <div id="parent">
  </div>
</div>
<div id="modal">
  This text is pretty long here.  Hope fully, we will get some scroll bars.
</div>

CSS (solo nuevo):

#context {
  position: absolute;
  overflow-y: scroll;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

3. No desplazarse por ningún lado excepto en el modal mientras esté activo

La solución anterior todavía permite que el elemento padre reciba eventos de desplazamiento, siempre y cuando no sean interceptados por la ventana modal (es decir, si se activan mediante la rueda del ratón mientras el cursor no está sobre la modal). Esto a veces es indeseable y puede que queramos prohibir todo desplazamiento del fondo mientras la modal está activa. Para hacer eso, necesitamos insertar un contexto de apilamiento adicional que abarque toda la ventana gráfica detrás de la modal. Podemos hacer eso mostrando una superposición posicionada de manera absoluta, que puede ser completamente transparente si es necesario (pero no visibility:hidden).

https://jsfiddle.net/0bqq31Lv/2/

HTML :

<div id="context">
  <div id="parent">
  </div>
</div>
<div id="overlay">  
</div>
<div id="modal">
  This text is pretty long here.  Hope fully, we will get some scroll bars.
</div>

CSS (nuevo encima de #2):

#overlay {
  background-color: transparent;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

Respuesta #7

Aquí tienes una versión de JavaScript puro:

function scroll(e) {
  var delta = (e.type === "mousewheel") ? e.wheelDelta : e.detail * -40;
  if (delta < 0 && (this.scrollHeight - this.offsetHeight - this.scrollTop) <= 0) {
    this.scrollTop = this.scrollHeight;
    e.preventDefault();
  } else if (delta > 0 && delta > this.scrollTop) {
    this.scrollTop = 0;
    e.preventDefault();
  }
}
document.querySelectorAll(".scroller").addEventListener("mousewheel", scroll);
document.querySelectorAll(".scroller").addEventListener("DOMMouseScroll", scroll);

Artículos relacionados