soy Kseso y esto EsCSS

Javascript con Furoya: control del scroll. Eventos y efectos al desplazar la página

Nuevo artículo de la serie Javascript con Furoya. Inicio de una serie dedicada al control del scroll o eventos relacionados con el desplazamiento de la página al hacerlo y actuar en función de ello.

Javascript con Furoya: control del scroll. Eventos y efectos al desplazar la página

✎ 15
COLABORACIÓN AUTOR INVITADO
Javascript con Furoya: control del scroll. Eventos y efectos al desplazar la página

Ya mencionamos (y usamos) algunos eventos de javascript. Aunque no los describimos con mucho detalle. Hay demasiado escrito en manuales y tutoriales, así que sólo vamos a darle un repaso somero antes de ver el próximo ejemplo para destripar.

Los eventos son detectados por el javascript de los navegadores cuando se produce un cambio en la página, generalmente por mano del usuario. Aunque hay algunos que podría decirse que son autónomos, como el caso de onload que se dispara cuando se carga un archivo, y en una página web puede ser el mismo documento o una img. En principio son atributos a incluir en las etiquetas de apertura del elemento, pero más tarde vamos a ver que pueden ubicarse en otras partes del código.

El mecanismo se parece mucho a las pseudoclases de CSS, pero tienen una diferencia básica: el CSS detecta un cambio de "estado" mientras que JS sólo ve el evento cuando se produce.

Eventos Javascript

Para dejarlo más claro, pensemos en un ejemplo típico. Con :hover aplicado a un elemento podemos cambiar su estilo (p.e. el color de fondo) al pasar el puntero sobre él, y devolverlo a su formato original simplemente retirando el cursor de encima. CSS revisa el estado del elemento constantemente, y sabe cuándo el puntero está dentro o fuera de sus límites para cambiar el color.

Con javascript necesitamos un evento llamado mouseover (podría ser otro, pero vamos a poner éste) que detecta cuándo el puntero deja de estar fuera del elemento y pasa a estar dentro de sus límites; pero no ve si sigue ahí o sale, lo único que hace es dispararse con el hecho o evento, y así ejecutar una función o línea de JS que cambie el background.

Si retiramos el puntero se queda igual, así que para emular el comportamiento de la pseudoclase de Css :hover tenemos que usar un segundo evento que se dispare cuando el puntero salga de los límites del elemento, y con otro escript lo cambie a otro color, que será el original porque es lo que nos conviene (obviamente, también podría ser un tercero).

<style> p {background-color: teal; } p:hover {background-color: lime; } </style> <p> hover </p> <p style="background-color:crimson" onmouseover="this.style.backgroundColor='hotpink';" onmouseout="this.style.backgroundColor='crimson';"> Un texto </p>

Algunos detalles. Las reglas Css (y sus declaraciones) que están en el elemento style no aplican al segundo párrafo ya que sus estilos estan "inline" y pesan más (son más específicos por su origen). El escript escribe (o reescribe) estos mismos estilos, así que siguen teniendo prioridad.

La palabra reservada 'this'

Y aunque el código es autoexplicativo voy a detenerme un poco en la palabra reservada this, que en este caso hace referencia al mismo elemento en el que está el evento.

Otra forma de escribir el valor del atributo onmouseover anterior sería:

onmouseover="document.getElementsByTagName('p')[1].style.backgroundColor='hotpink';"

Y una tercera:

onmouseover="document.querySelectorAll('p')[1].style.backgroundColor='hotpink';"

y hasta hay otra más:

onmouseover="document.querySelector('p:nth-of-type(2)').style.backgroundColor='hotpink';"

por no exagerar con :nth-child(n), previo conteo de la cantidad de elementos del padre.

Pero es más que evidente que usar this resulta más cómodo.

Otro detalle a observar es el uso de comillas simples y dobles, para que no interfieran entre ellas al poner límites. Una forma práctica de evitar este conflicto es poner como valor del atributo una función, donde podemos mandar el this como argumento.

<script type="text/javascript"> function encimaDe(miArgumento) { miArgumento.style.backgroundColor='hotpink'; } </script> <p style="background-color:crimson" onmouseover="encimaDe(this)" onmouseout="this.style.backgroundColor='crimson';"> events </p>

Ese argumento this se recibe en la función que está entre etiquetas de script usando una variable (para el ejemplo le puse miArgumento) que va a estar entre los paréntesis. A partir de allí, la variable pasa a valer lo mismo que el elemento.

Debo advertir que ya no se recomienda escribir eventos como atributos, dentro de las etiquetas. Aprovechando la capacidad de javascript para escribir al vuelo en el documento, los eventos se ponen a través de addEventListener y así no quedan visibles en el código fuente. Pero eso es algo que trataremos más adelante.

onscroll

Como dije, los eventos se disparan una vez y los escripts que ejecutan no se repiten mientras el evento no se repita también. Pero el ejemplo práctico que veremos hoy usa un evento algo particular, porque se tiene que disparar repetidas veces, y debemos evitarlo.

Es onscroll y lee el desplazamiento del documento, el hecho de moverse en alguna dirección. Y ya suponemos el problema, si arrastramos el botón de la barra, el evento se dispara cuando el navegador siente el desplazamiento; pero como éste se hace por pasos (en la configuración del navegador o del sistema operativo se especifica cuánto mide cada paso) al superar esa medida el evento se va a disparar de nuevo.

En realidad puede ser peor, ya que el evento podría ser más preciso y detectar el arrastre por el tiempo que tarda javascript (con la velocidad que le permita el microprocesador) en leer el evento, y una vez hecho quedar a la espera de un nuevo arrastre. Como la velocidad de respuesta es de milisegundos, al mover el botón "un rato" estaríamos mandando una metralla de eventos.

Esto no debería ocurrir si usamos el teclado, con [Av.Pág.], [Re.Pág.], [Inicio] o [Fin]; así que los navegadores se las arreglan para que en estos casos se reciba un solo evento.

No hay una norma, y cada fabricante lo hará como a sus programadores mejor les parezca.
Y tampoco hay una función nativa de JS que ignore los eventos repetidos en poco tiempo, así que para el siguiente ejemplo, la vamos a hacer nosotros.

Hace unos meses comentábamos que los encabezados fijos tienen un problema: tapan el comienzo del documento cuando avanzamos de a una página (Position-sticky...). Hay varias maneras de resolverlo, y yo me inspiré en la del sitio de CÑÑ para ésta.

La idea es detectar cuándo el documento se está desplazando, y mantener el encabezado; cuando se detenga se lo hace desaparecer para que el documento quede "limpio". Por supuesto, si movemos el puntero hasta arriba, reaparece, y si estamos en el inicio, se mantiene. Todo está hecho con CSS, como corresponde a este blog ... excepto la lectura del desplazamiento, que no se puede capturar con una pseudoclase, por lo que nosotros terminamos asignando una clase (real) con javascript.

Veamos el ejemplo. Juega con él. Y tras ello la explicación:

See the Pen The ghost head(er). by solipsistaCP (@solipsistacp) on CodePen.

var tiempo; function desplaza() { clearTimeout(tiempo); tiempo = setTimeout(oculta, 400); document.querySelector("header").className = "aparece"; } function oculta() { if((document.documentElement.scrollTop || self.pageYOffset) != 0) { document.querySelector("header").className = "desaparece"; } } onscroll = desplaza;

Aquí ya tenemos un caso en que el evento no es un atributo. La función desplaza() no se ejecuta desde la etiqueta body sino que está en el mismo bloque de escripts. Entonces no necesita paréntesis, y como se supone que por omisión se aplica al objeto window, desde allí puede perfectamente "ver" el desplazamiento del documento.

Las clases aparece y desaparece son las que cambian la visibilidad del header, el :hover nos ahorra los onmouseover y onmouseout. Y el resto te lo explico ahora.

Hay una variable global vacía declarada al comienzo, que nos va a servir para demorar el evento y evitar que se dispare repetidas veces cuando arrastramos la scrollbar. El truco está en dos funciones propias de javascript:

  1. setTimeout(aEjecutar, temporizador) que empieza un conteo de milisegundos (el último argumento) y al terminarse ejecuta una línea o función JS (el primer argumento). Ahí vemos que lo que va a hacer es disparar una función oculta() en 400 milisegundos. Este tiempo debe ser mayor al que se demore el navegador en leer cada evento. Hoy con cuatro décimas de segundo alcanza, pero si se usara en máquinas viejas, quizás habría que ponerle un poco más.
    Inmediatamente después (aún antes de ejecutar la función oculta(), por su retraso) hace visible (o mantiene visible) el encabezado.
  2. clearTimeout(varQueContieneTimeoutADetener) que está puesta antes de setTimeout() la que, como se ve, estaba metida dentro de la variable global. Su propósito es detener la función (a través de la variable que la contenga) que esté entre los paréntesis. De esa forma, lo primero que hace es "limpiar" o borrar la función de conteo (si no existe, como sería con la primera vez que se ejecute toda la función, la ignora y sigue).

El script paso a paso

Estudiando paso a paso el escript, vemos que el comportamiento es muy simple.

  • Al arrastrar la barra de desplazamiento disparamos el evento, que ejecuta la función desplaza(), primero detiene un conteo que por ahora es inexistente, luego pone en la variable tiempo un contador que va a ejecutar otra función oculta() 400 milisegundos después. Pero antes de que vaya a ocurrir, pone una clase al header para que se vea —si es que está oculto— o se mantenga visible si ya tiene esa clase.
  • Al seguir arrastrando el botón de la barra, es seguro que disparamos nuevamente onscroll antes de 400ms, por lo que previo a que se ejecute oculta() ya ejecutamos nosotros de nuevo desplaza(), y entonces clearTimeout(tiempo) detiene el conteo para que no se ejecute la función que esconde el encabezado, que estaba corriendo de la vuelta anterior. Luego otra vez se inicia un conteo para ocultarlo y se repone (por las dudas) la clase para mostrarlo.
  • Si seguimos arrastrando el documento, el proceso se mantiene con el reseteo del cambio. Pero en algún momento vamos a detener el desplazamiento, y entonces sí, después de 400ms la función oculta() se va a ejecutar.
  • Y si la posición no es igual a 0 (cero) porque no está al comienzo del documento, entonces el header cambia de clase y desaparece; si el documento está "arriba", no se cambia nada para que se mantenga visible.

Ya vimos el uso de condicionales, y la mayoría se dio cuenta de cuál es esa condición que tiene puesto el escript. Dijimos que el evento sabe que estamos desplazando el documento, pero no dice cuánto ni en qué dirección. Con document.documentElement.scrollTop (para algunos navegadores) o con self.pageYOffset (para otros) podemos saber cuántos pixeles está desplazado verticalmente el documento. Si es 0; entonces está arriba, y si no es 0; entonces está a mitad de camino, y el escript cambia la clase.

Por supuesto, esa doble barra vertical || es el operador OR ("o inclusivo"). Si un navegador no interpreta una instrucción, prueba con la otra. Alguna tiene que funcionar.

Como en los anteriores ejemplos, sin tener conocimientos de programación éste seguro que tampoco se entiende a la primera leída; así que recomiendo ver el pen en funcionamiento, y después releer el artículo. Entonces la mecánica va a aparecer mucho más clara.

En el próximo seguimos exprimiendo los usos de onscroll, con fines un poco más prácticos: Parallax y Lazy-Load ¿estoy dentro o fuera del viewport?

Autoría

Furoya: Autor del artículo

Artículo original de Furoya.
La intención del autor con sus colaboraciones no es que los artículos sirvan para hacer un copy&paste de códigos sino que comprendas y aprendas la lógica y el cómo trabaja javaScript.
Y a partir de lo expuesto experimentes tú.
El autor del post y el editor del blog te animamos a que plantees tus dudas o reflexiones y que compartas tus realizaciones en base a lo expuesto en los comentarios. Recuerda que puedes incluir pens (ejemplos en Codepen.io) en ellos.