Una cuestión peliaguda de resolver por medios tradicionales es la que encabeza este artículo: ¿Cómo puedo detectar si el usuario actual tiene nuestra página oculta o visible?.
Esto nos puede servir para muchas cosas, como por ejemplo:
- Dejar de hacer llamadas en segundo plano al servidor para obtener datos cuando el usuario no esté usando la aplicación web, ya que no habrá nadie para verlos. Así que nos ahorraremos carga innecesaria nuestros servicios. Típico en aplicaciones que utilizan AJAX.
- Si el usuario está reproduciendo un vídeo o cualquier otro elemento (por ejemplo, una presentación), podemos detenerlo mientras no está mirando, dejando que continúe automáticamente cuando el usuario vuelva a la página.
- En una aplicación de email, cuando el usuario esté mirando la página podemos verificar si hay nuevos correos cada pocos segundos. Si no está en primer plano bajamos el periodo de comprobación para hacerlo cada pocos minutos y ahorrarle carga al servidor.
- También podemos usar notificaciones cuando esté oculta, obviándolas cuando el usuario esté viéndola.
Tradicionalmente lo que se solía hacer para conseguir algo parecido a esto era tratar de detectar el evento blur de la ventana (objeto window) para que cuando perdiese el foco lo detectásemos, y luego el evento focus de la misma para cuando se volviese a recuperar. Pero este método es bastante imperfecto ya que, por ejemplo, si queremos colocar una ventana en un monitor para verla de vez en cuando, ésta perdería el foco pero realmente estaría visible, así que usar este método del foco haría que la aplicación no funcionase adecuadamente.
Para ayudarnos con este problema el W3C ha definido una API de visibilidad que, aunque no forma parte formal del estándar HTML5, su especificación está ya terminada y aprobada hace tiempo (como recomendación) y en la actualidad está soportada por todos los navegadores modernos.
Su uso es realmente sencillo. Consta de dos propiedades y un manejador de eventos para el documento actual, a saber:
- hidden: devuelve un booleano especificando si la página está actualmente visible o no.
- visibilityState: devuelve una cadena de texto identificando el estado actual de la página en cuanto a su visibilidad. Sus valores posibles son:
- "visible": cuando la página actual está "visible" para el usuario (ahora explico por qué lo pongo entre paréntesis).
- "hidden": cuando la página está "oculta" para el usuario (idem).
- "prerender": cuando la página todavía se está renderizando y por lo tanto no es visible para el usuario (a todos los efectos está oculta, y hidden vale false). Este estado solo se da durante unos instantes al principio y no se puede volver a cambiar al mismo. Su soporte es opcional, por lo que no todos los navegadores lo ofrecen. Su aplicación es rara.
- "unloaded": cuando la página se está descargando de la memoria. Al igual que la anterior su soporte es opcional, por lo que no podemos contar con ella (y tampoco suele ser muy útil).
- visibilitychange: el nombre del evento que salta cuando el estado de visibilidad (la propiedad anterior) cambia.
¿Cuándo está oculta una página?
Antes de ver un ejemplo práctico es necesario aclarar qué significa "visible" y "oculto" a efectos de esta API, ya que puede que no signifique lo mismo que para ti.
La API considera que una página está oculta cuando está en una pestaña del navegador que no es la que estamos viendo o cuando la ventana está minimizada. Y considera que una página está visible cuando ésta es la pestaña actualmente activa de una ventana no-minimizada.
Esto es muy importante porque significa que si nuestra página está en una pestaña activa del navegador, pero éste no está minimizado, aunque tengamos otras ventanas por encima y realmente no se vea, la API la sigue considerando como una página visible. Es decir, en la práctica para que el estado de una página cambie a oculto es necesario o bien cambiar la pestaña actual o bien minimizar el navegador. Cualquier otra cosa no cambia el estado. Es muy importante tenerlo claro.
El motivo de hacerlo así, supongo, es lograr que el usuario pueda cambiar entre ventanas o tener nuestra página abierta en otro monitor, etc... sin que se cambie su estado de visibilidad. En el caso de un navegador móvil esto no tiene tampoco mucha vuelta de hoja, ya que solo pueden tener una aplicación abierta a la vez y las ventanas no se solapan. Pero yo me imagino que en un navegador de escritorio, hacer un cálculo de si una página está tapada por otras ventanas y que funcione en todos los sistemas operativos es algo innecesariamente complicado. Además existe la disyuntiva de decidir cuándo una página está realmente tapada en ese caso: ¿qué % de su superficie debería estar tapada para considerarla oculta? ¿cuándo esté totalmente tapada? ¿un determinado %? Dado que las páginas pueden ser muy largas y hacer scroll, ¿por qué deberíamos considerar solo el área visible de la misma?
Todos estos problemas se solucionan con el criterio que describo más arriba y que es el que se ha adoptado. Puede que no sirva para todos los casos, pero entonces siempre podemos combinarlo con los eventos blur y focus de la ventana y ya podríamos tener todos los casos cubiertos.
Un ejemplo práctico de uso
Para ver su uso en la práctica he preparado un ejemplo muy sencillo que puedes descargar desde aquí (ZIP, 915 bytes).
Se trata de un simple contador que aumenta en 1 cada segundo. El contador detecta cuando la página de estar visible y para de contar, retomando la cuenta cuando volvemos a la página:
Además hace un log de todas las veces que se muestra y se oculta.
El código es bastante sencillo -tan solo didáctico para mostrar el uso de la API- pero paso a explicarlo a continuación:
- En el HTML hay un span llamado "contador" y un div llamado "log" que se usan -obviamente- para mostrar el contador y el log.
- Al cargar la página se llama a la función empezarContador que tiene el siguiente código:
function empezarContador(){
if (typeof(document.hidden) != undefined) {
document.addEventListener("visibilitychange", cambiaVisibilidad, false);
tick();
}
else
document.getElementById("contador").textContent = "¡¡Este navegador no soporta la API de visibilidad!!";
}
Lo que se hace es comprobar si la característica está o no soportada por el navegador actual y si lo está se define un manejador para el evento visibilitychange y se inicia un contador (tick()).
Es interesante destacar la manera poco habitual en la que se determina el soporte de la propiedad hidden. Normalmente para comprobar si una propiedad o método están soportados se suele escribir un condicional muy sencillo parecido a este (detalles sobre este asunto que quizá no conozcas):
if (document.caracteristica)
... //Se soporta
else
... //No se soporta
Pero en este caso no es posible hacerlo así ya que "hidden" es una propiedad booleana y no funcionaría. Así que comprobamos si está definida o no usando su tipo. También podríamos haber usado la otra propiedad visibilityState, en cuyo caso el condicional se haría de la forma convencional.
- En la función tick lo único que hacemos es crear un temporizador que llama a la misma función cada segundo solo si la página está visible:
function tick(){
cuenta++;
document.getElementById("contador").textContent = cuenta;
if(!document.hidden)
idTimer = setTimeout(tick, 1000);
}
- Finalmente, para hacer el log, manejamos el evento visibilitychange de la siguiente manera:
function cambiaVisibilidad(){
if(document.hidden){
clearTimeout(idTimer);
log += "Se ha OCULTADO a las: " + new Date().toLocaleTimeString() + "<br/>";
}
else {
log += "Se ha MOSTRADO a las: " + new Date().toLocaleTimeString() + "<br/>";
tick();
}
document.getElementById("log").innerHTML = log;
}
Esta función de llama automáticamente cada vez que hay un cambio en el estado de visibilidad. Comprobamos si está oculta o visible. Si está oculta detenemos el temporizador (es necesario para evitar que cambios muy rápidos de estado de visibilidad lleguen a lazar más de uno y el contador vaya más rápido) y "logueamos" el cambio. Si está visible de nuevo registramos el cambio y llamamos de nuevo a tick para reanudar el contador.
Ejecuta la página y pruébala. Para ello cambia de pestaña en el navegador o minimízala y déjala unos segundos oculta. Verás que se detiene el contador y se registra el cambio de estado. Sin embargo ahora simplemente ponla en otro monitor o ponle una ventana delante (pero no la minimices): verás que ahora el estado no cambia.
Soporte en algunos navegadores antiguos
Aunque ya digo que está soportada por todos los navegadores modernos, si queremos dar cobertura a navegadores antiguos podemos usar prefijos propios de cada navegador para poder sacarle partido en versiones muy viejas de algunos de ellos.
El uso es exactamente el mismo, pero los nombres de las propiedades y del evento cambian según el navegador:
- Firefox: el prefijo sería "moz": mozHidden, mozVisibilityState y mozVisibilitychange.
- Chrome: el prefijo era "webkit": webkitHidden, webkitVisibilityState y webkitVisibilitychange. Se soportaba en la versión 31. A partir de la 33 ya se soportaba el estándar. Raro que necesitemos usarlo. En la versión 38 había un bug que hacía que el evento saltase dos veces.
- Android: en el navegador por defecto de Android se soportaba el prefijo "webkit" en las versiones 4.4 hasta la 4.4.4. La actual ya soporta el estándar.
- Internet Explorer: el prefijo era "ms": msHidden, msVisibilityState y msVisibilitychange. Este prefijo se usaba en IE 8 y IE9. En IE10 ya se soportaba el estándar. Otra particularidad de estas versiones viejas es que msVisibilityState devolvía un número para cada estado, en vez de una cadena por lo que si lo vas a usar para navegadores antiguos mejor que uses msHidden que se usa de la misma forma que el del estándar.
¡Espero que te resulte útil!