Javascript con Furoya: control del scroll. Eventos y efectos al desplazar la página 25.5.15
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
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:
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ónoculta()
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ónoculta()
, por su retraso) hace visible (o mantiene visible) el encabezado.clearTimeout(varQueContieneTimeoutADetener)
que está puesta antes desetTimeout()
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 variabletiempo
un contador que va a ejecutar otra funciónoculta()
400 milisegundos después. Pero antes de que vaya a ocurrir, pone una clase alheader
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 ejecuteoculta()
ya ejecutamos nosotros de nuevodesplaza()
, y entoncesclearTimeout(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
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.
Ala que bueno todo esto, gracias Furoya por compartirlo, hace 4 meses esto me hubiese venido de perlas embes del trágico plugin que use aquella vez. En fin, tienes mi mas sincera gratitud mas que nada porque yo no salgo de css y solo se una que otra cosa de jquery y php así que espero seguir viendo más seguido tus experimentos por aquí. Saludos a Kseso por difundir. Birras para todos!!
ResponderEliminar¡Llegué 4 meses tarde! Lo siento, especialmente porque este ejemplo es casi todo CSS, y el JS fácil de entender y adaptar a las distintas necesidades. El problema de todos los plugin o frameworks es que por más bien hechos que estén siempre tienen código que uno no va a usar, y no es tan grave el peso extra, como los conflictos que puedan aparecer si se los combina con otros. Entonces sí la carga de varios y sus correspondientes "compatiblizadores" se vuelve una tragedia.
EliminarSegún el caso, habrá que agregar alguna línea más, porque estos ejemplos son muy simples; pero sirven de inicio, de base para entender el funcionamiento y tomarlos como incentivo para buscar en los manuales otras cosas que hagan falta para cada proyecto. Si te sirven para estudiarlos, el agradecido soy yo, porque entonces valió la pena molestar a Kseso para empezar a mostrar en su blog cómo se modifica un estilo valiéndose de javascript.
Porque al fin y al cabo, éste es un blog de CSS.
Gracias por la birra, y un saludo para vos también.
hola, como hago que se aplique a un div en lugar de un selector? es decir que no se aplique al selector head si no a cualquier elemento tipo div.
ResponderEliminarHola Carlos
EliminarA riesgo de que Furoya me corra a gorrazos yo diría que sólo necesitarías modificar el elemento al que apunta la propiedad document.querySelector("header")
Un saludo
¿Como puedo implementarlo para varios selectores o divs al mismo tiempo? es decir en un mismo html, e intentado pero no sale.
ResponderEliminarPara estas consultas tan específicas de un caso concreto, Carlos, lo más indicado es que las acompañes de un enlace a tu realización.
EliminarHoy por hoy, con servicios en línea como Codepen, es de lo más sencillo.
A ello se suma la posibilidad que tenéis de incluir pen´s de codepen en los comentarios.
Un saludo
Habiendo sido respondida con precisa corrección la pregunta anterior, voy a publicar mis comentarios a partir de ésta. Me gustaría ver a mí también un ejemplo para el caso, porque la idea original es usarlo en un 'header'. Y no debería haber más de uno, aunque esté hecho con un 'div'. Si dentro hubiese otros elementos cuyas animaciones sean diferentes (en tiempo, transparencia, posición,...), lo mismo se cambia la clase del contenedor y en la hoja de estilos se ponen las animaciones de propiedad para cada clase en los elementos hijos precedidas, justamente, por las de su padre en cada estado.
EliminarHemos venido a jugar, así me la juego xD que dicen en los concursos =P
Eliminar¿Me equivocaré mucho si pienso que Carlos estás intentando usarlo para mostrar el típico elemento emergente al alcanzar cierto punto el scroll?
Esos que sacan un modal o cuadro de sugerencias ya sea por un lateral o sobrepuestos a todo el viewport.
Bueno, aquí quedan mis 50cnts
Ajá. Algo parecido a
Eliminar...¿estoy dentro o fuera del viewport?
Si no fuese el caso, aprovecho para comentar que los carteles emergentes para cada posición del documento no precisan realmente de varios 'div'; puede ser siempre el mismo, y se cambia el contenido según se necesite.
Lo que seguro voy a hacer es aprovechar la consulta para recordar que estos artículos no son "cutanpaste", y que si funcionan es para su ejemplo específico. Si alguien los quiere modificar, tiene que aprender un poco de javascript.
Que no es novedad, así es siempre aunque algunos quieran simular que sus códigos se pueden "ensamblar" a otros sin ningún inconveniente.
Lo raro es que no aparezca ningún inconveniente. :-P
Hola Unknown. (mmmm... va a haber que empezar a numerarlos...)
ResponderEliminarEstás hablando de un escript que no conozco, si se ejecuta al cargar la página es que lo hiciste para ejecutarse al cargar la página. De otra forma lo haría con otro evento.
Como el de scroll, que se ejecuta repetidas veces y así te permite confirmar si la distancia desplazada coincide con la posición de un elemento, y entonces mostrar eso que querías mostrar con tu escript.
Hay ejemplos en enlaces de este mismo artículo, y también puse otro en el anuario.
Pero como no estás mostrando un ejemplo que podamos probar, ni estás describiendo dónde está la falla (encima la estás complicando con un ... ¿contador?) ya es imposible colaborar más a tu situación.
Aunque más no sea por curiosidad, a mí me gustaría saber qué están haciendo.
Grande Furoya! Cuando lo leí por primera vez, y más allá del ejemplo, me preguntaba para que me podría servir...seguro de que para algo bueno me iba a servir. Ahora consigo lanzar los eventos según la posición del scroll...Gracias Tronco!!!!
ResponderEliminarDe nada, Xers.
EliminarSiempre insisto en que la idea de estos inventos es que los desarmen para adaptarlos a sus necesidades. Entonces me alegra que éste te haya servido.
Hola, que tal, ayer estaba revisando tu blog. Esta bueno, yo leo algo de jquery, ayuda mucho poder encontrar ejemplos específicos donde poderse desarrollar, además de usarlos inmediatamente en proyectos. Saludos
ResponderEliminar"...En el próximo seguimos exprimiendo los usos de onscroll, con fines un poco más prácticos."
ResponderEliminarWhere is it?
Hola Jofret.
EliminarEncantado que encuentres algo útil en el blog.
¿Que dónde está la continuación? Creo que buscas éste artículo que casualmente en estos momentos lo he vuelto a llevar al índex.
Gracias por darte cuenta de mi despiste, como editor, de no haber actualizado el artículo al salir su continuación el mes siguiente.
Un saludo.
P.D.: ahora que repaso el hilo de comentarios veo que Furoya en el 3.4 ya lo había enlazado también.