IMPORTANTE: he actualizado el post y el proyecto y he creado la versión 3.0 de la biblioteca que añade 4 eventos a cualquier elemento de una página para notificar cuando se muestra, se oculta, total o parcialmente, siendo mucho más fácil de utilizar. IR AL POST NUEVO.
----------
Una cuestión que puede resultar muy útil en una página o aplicación web es la posibilidad de detectar cuándo aparece o desaparece de la pantalla un elemento determinado debido a las acciones del usuario.
Por ejemplo, si desaparece una pieza de información importante porque el usuario hace scroll moviendo los contenidos, podemos sacar una nota resumen, recordatorio o acceso directo para ir verla de nuevo, y ocultarlo otra vez cuando vuelva a aparecer. Cosas por el estilo.
Para conseguir algo así nos vendría muy bien disponer de un evento inViewport o similar que nos informase de cuándo un elemento aparece o desaparece de la parte visible de la página. Nos suscribiríamos a este evento y recibiríamos automáticamente notificaciones si el elemento aparece o desaparece.
El problema es que no existe ningún evento como este en HTML/JavaScript.
No nos queda más remedio que buscarnos la vida para poder disponer de una funcionalidad similar a esta.
En este artículo vamos a desarrollar desde cero la funcionalidad necesaria para conseguir tener un evento como este, listo para utilizar en cualquier página.
La funcionalidad la vamos a dividir en varios pasos individuales que, en conjunto, nos permitirán obtener lo que necesitamos.
1.- Detectar la ubicación relativa de cualquier elemento de la página
En HTML la forma de detectar la ubicación real de un elemento en la página es utilizar el método getBoundingClientRect del que disponen todos los elementos del DOM.
Este método no toma ningún parámetro y devuelve un objeto especial de tipo DOMRect que, como se puede deducir de su nombre, contiene información sobre los bordes que definen el área del elemento en cuestión. En concreto dispone de 4 propiedades que marcan a posición de cada uno de los bordes del modelo de caja del elemento respecto al viewport actual: left, top, right y bottom:
var caja = elto.getBoundingClientRect();
console.log(caja.top);
Mostraría en la consola la posición del borde superior del elemento.
2.- Determinar si el elemento está visible en la página o no
Como las coordenadas anteriores son relativas al área visible (o viewport) de la página, esto quiere decir que varían en cada momento en función de donde esté colocado el elemento. Si hacemos scroll irán cambiando. Si el elemento desaparece por la parte superior de la página la propiedad top devuelta por esta función empezará a ser negativa, por ejemplo.
Podríamos determinar fácilmente si el elemento está o no dentro de la página en un momento dado definiendo una función como esta:
function isElementTotallyVisible(elto) {
var anchoViewport = window.innerWidth || document.documentElement.clientWidth;
var alturaViewport = window.innerHeight || document.documentElement.clientHeight;
//Posición de la caja del elemento
var caja = elto.getBoundingClientRect();
return ( caja.top >= 0 &&
caja.bottom <= alturaViewport &&
caja.left >= 0 &&
caja.right <= anchoViewport );
}
Lo que hacemos es, antes de nada, determinar el ancho y el alto del área visible actual, para lo cual usamos la propiedad innerWidth y innerHeight de la ventana.
Nota: Se incluye también el clientWidht y clientHeight del documento en caso de que no esté soportado lo anterior para dar soporte a versiones antiguas de Internet Explorer (anteriores a la 9). getBoundingClientRect está soportado incluso en versiones muy antiguas de IE porque es un invento inicial de Microsoft que luego incorporaron los demás navegadores.
Luego se obtiene los límites de la caja del elemento, y se compran con los bordes del viewport para ver si el elemento está totalmente contenido dentro de éste o no.
Devuelve true en caso afirmativo y false en caso de que se salga, aunque sea un poco, por cualquier lado.
Es un poco más complicado determinar si el elemento está contenido parcialmente en la página o no. A lo mejor nos interesa determinar no solo cuando cualquier parte del elemento salga de la vista (que sería lo anterior) sino cuando se salga de la vista por completo. En este caso sería útil averiguar si el elemento está dentro del área visible aunque sea parcialmente. Es decir, con que se vea aunque sea un fragmento minúsculo del elemento, poder saberlo.
Tendríamos que crear una versión alternativa de la función anterior cambiando la comprobación final, que ahora sería como esta:
function isElementPartiallyVisible(elto) {
var anchoViewport = window.innerWidth || document.documentElement.clientWidth;
var alturaViewport = window.innerHeight || document.documentElement.clientHeight;
//Posición de la caja del elemento
var caja = elto.getBoundingClientRect();
var cajaDentroH = (caja.left >= 0 && caja.left <= anchoViewport) ||
(caja.right >= 0 && caja.right <= anchoViewport);
var cajaDentroV = (caja.top >= 0 && caja.top <= alturaViewport) ||
(caja.bottom>= 0 && caja.bottom <= alturaViewport);
return (cajaDentroH && cajaDentroV);
}
En este caso las condiciones son más complicadas. Básicamente vemos, por un lado, si está dentro del área visible horizontalmente, y por otro si está dentro (aunque sea por poco) en el eje vertical. Se considera que está dentro si hay algo del elemento dentro del viewport tanto en horizontal como en vertical. Esto es así ya que no llega solo con que esté en uno de los ejes. Por ejemplo, si está en el eje vertical, pero no en el horizontal significa que está a la altura correcta pero con un scroll horizontal que lo ha sacado de la parte visible. Es fácil de ver si haces unas pruebas.
3.- Detectar automáticamente si el elemento aparece o desaparece del área visible
Lo primero es determinar por qué causas es posible que un elemento se desplace, y por lo tanto pueda aparecer o desaparecer de dentro del área visible de la página.
Un primer caso es el evidente: un scroll de la página. Esto lo podemos detectar gracias al evento scroll del mismo nombre de la ventana.
Además del scroll hay otras causas para la modificación de la posición de un elemento:
- Cambio de tamaño: si la ventana cambia de tamaño, la página se redibuja (refluyen los elementos) y la posición de todos ellos cambia. Esto se puede detectar con el evento resize de la ventana.
- Carga de la página: la carga de la página se produce realmente en dos fases. Primero se carga su código y se interpreta (el DOM estaría listo) y luego se acaban de cargar los elementos externos, como por ejemplo las imágenes. Si alguna de estas imágenes no tiene especificadas sus dimensiones, no ocupará su verdadero sitio hasta que acabe de cargar, desplazando otros elementos al fijarse su tamaño final. Lo detectaríamos en el evento load de la página.
Hay algún caso más que veremos un poco después, pero con estos 3 sería suficiente para la mayor parte de las necesidades.
¿Cómo aprovechamos estos eventos para detectar lo que nos interesa?
Vamos a definir una función que nos va a permitir definir una especie de evento para detectar lo que necesitamos. La vemos y analizaremos su código:
function inViewportPartially(elto, handler) {
var anteriorVisibilidad = isElementPartiallyVisible(elto);
//Defino un manejador para determinar posibles cambios
function detectarPosibleCambio() {
var esVisible = isElementPartiallyVisible(elto);
if (esVisible != anteriorVisibilidad) { //ha cambiado el estado de visibilidad del elemento
anteriorVisibilidad = esVisible;
if (typeof handler == "function")
handler(esVisible, elto);
}
}
//Asocio esta función interna a los diversos eventos que podrían producir un cambio en la visibilidad
window.addEventListener("load", detectarPosibleCambio);
window.addEventListener("resize", detectarPosibleCambio);
window.addEventListener("scroll", detectarPosibleCambio);
}
Esta función sirve para definir un manejador para nuestro evento, al que hemos llamado inViewportPartially. Este evento llamará a nuestro manejador cuando el elemento que le pasemos como primer parámetro entre o salga de la zona visible de la página. Al llevar "parcial" en el nombre quiere decir que con que esté parcialmente dentro de la página, se considerará que está dentro del área visible. Es decir que para que nos diga que el elemento NO está visible, deberá desaparecer totalmente, y para decirnos que está visible llega con que se vea aunque solo sea un pixel del mismo.
Veamos cómo funciona.
La función recibe como parámetros el elemento a monitorizar y la función que se ha de llamar cuando el elemento entre o salga del área visible de la página.
Lo primero que hacemos es establecer el estado inicial de la visibilidad del elemento, es decir, si cuando se llama a esta función el elemento está visible o no, que será nuestro estado de base para el mismo. Se consigue con la función vista en el paso 2, y en este caso consideramos elementos parcialmente visibles.
Definimos también una función dentro de esta función que será la que se llamará ante cualquiera de los 3 eventos que hemos considerado que cambian el estado de visibilidad del elemento (load, resize y scroll). Esta función (detectarPosibleCambio) lo único que hace es comprobar el estado actual de visibilidad del elemento y compararlo con el estado de base. Si ha cambiado anota el nuevo estado base (para la siguiente comparación, cuando proceda), y llama al manejador que hemos especificado (handler) pasándole como parámetros un booleano que indica si el elemento está en el área visible o no, y una referencia al elemento cuyo estado ha cambiado (para poder usar el mismo manejador para varios elementos).
Nota: Este método interno tiene acceso a los parámetros de la función (elemento y manejador) y al estado de base gracias a la magia de las clausuras en JavaScript.
Finalmente lo único que se hace es definir estos tres eventos que habíamos identificado para que llamen a esta función interna ante cualquier cambio que se produzca.
También he definido en la biblioteca otra función prácticamente idéntica llamada inViewportTotally que es igual a esta pero comprueba si el elemento está dentro del área visible en su totalidad, y no solo parcialmente. En este caso en cuanto el elemento desaparece de la vista aunque sea un poco se notifica que ha desaparecido (pero todavía se verá parcialmente), y cuando vuelva a estar completamente dentro del área visible se notificará con un true que ha vuelto a aparecer. Es mucho más estricto que el evento anterior.
Nota: Otras formas de cambiar la disposición de la página
Además de los 3 eventos que hemos considerado, existen otras maneras menos frecuentes de que se cambie la posición de los elementos de la página. Concretamente tres casos que no vamos a contemplar en este ejemplo:
- La modificación mediante código del árbol de elementos de la página (DOM): si metemos o quitamos elementos éstos podrían provocar el reflujo de la misma y por lo tanto cambiar la posición de un elemento que nos interese. Si tuviésemos interés en detectar este hecho podríamos usar el evento DOMSubtreeModified o sus dos relacionados (DOMNodeInserted y DOMNodeRemoved). Lo consideraremos un caso poco frecuente y no lo incluiremos en el ejemplo.
- Cambio dinámico de la propiedad display de algún elemento: si cambiamos mediante JavaScript el modo de visualización de algún elemento de la página (por ejemplo para ocultarlo y que no ocupe espacio en la página, o hacer que se comporte como elemento de bloque cuando antes era inline). No existe ningún evento que nos informe de un cambio como este, por lo que no quedaría más remedio que usar un temporizador y verificar dentro de éste la posición de los elementos que nos interesen.
- Zoom de la página: si el usuario hace zoom en la página el tamaño de los elementos cambia y por lo tanto cambian de posición. Esto es más común en navegadores móviles, donde se hace zoom con los dedos, pero en los navegadores de escritorio también ocurre. No hay manera de detectar esto con garantías en un navegador. Por lo tanto deberíamos recurrir también a un temporizador que cada medio segundo (o el periodo que nos sirva para nuestro caso) usara las funciones descritas en el paso 2 para ver si el elemento está en el viewport o no. Poco recomendable para el beneficio que obtenemos, ya que en cuanto el usuario mueva la página se notificará con lo que tenemos ahora (y la moverá casi seguro si hace scroll con los dedos).
Aunque nuestro código no detecta estos tres casos marginales, en cuanto el usuario mueva mínimamente la página el evento de scroll detectará la posición y nos notificará de cualquier posible cambio que hubiese, así que en la práctica no compensa preocuparse por ellos.
Descarga y uso de la biblioteca
Vale, una vez que ya he explicado cómo he creado la biblioteca para simular estos eventos y cómo funciona por dentro, es hora de ver en la práctica cómo sacarle partido.
Lo primero es descargarla. La he puesto en GitHub como código abierto.
Descárgate el archivo con el código fuente (ZIP, 5.82KB). He incluido dos versiones:
- DetectarVisibilidad.js: es la versión completa y comentada del código. He usado documentación JSDoc para facilitar su uso desde editores capaces de interpretarlo para dar ayuda contextual (como Visual Studio o VSCode).
- DetectarVisibilidad.min.js: esta es la versión minimizada del anterior. Pesa tan solo 1,02KB, por lo que se descarga muy rápido y es la ideal para usar en sitios en producción.
Se exponen 4 funciones globales (podía haberlo modularizado para ser utilizado a través de una clase o con AMD y similares, pero he optado por no hacerlo dada su sencillez y particularidad):
- isElementTotallyVisible: indica si un elemento está o no, aunque sea un poco, dentro del área visible de la página.
- isElementPartiallyVisible: indica si un elemento está o no dentro por completo del área visible de la página.
- inViewportPartially: sirve para definir un manejador de evento que saltará automáticamente cuando el elemento que se indique esté dentro parcialmente o fuera totalmente de la página actual.
- inViewportTotally: sirve para definir un manejador de evento que saltará automáticamente cuando el elemento que se indique esté dentro totalmente o fuera parcialmente de la página actual.
Para utilizarlo solo hay que meter una referencia al script en la cabecera de la página y luego definir el evento de un modo similar a este:
var miDiv = document.getElementById("miElemento");
inViewportPartially(miDiv, cambiaVisibilidad);
lo cual hará que se llame a la función cambiaVisibilidad cada vez que el elemento entre parcialmente o salga del todo del área visible de la página.
Nota: La definición del evento se debe hacer al final de la página, donde los elementos que vamos a monitorizar están totalmente definidos y podemos localizarlos.
En la descarga se incluye también un pequeño ejemplo con dos cajas metidas en el medio de mucho texto (para poder hacer scroll con ellas y probarlo) y se usan los dos eventos (total y parcial) para mostrar en la consola del navegador cuando se produce el evento de entrada y salida de la zona visible, y así poder probarlo en la práctica.
También lo he colgado en GitHub (en inglés) en forma modular (que no interfiere en variables globales de la página).
¡Espero que te resulte útil!