Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:eventos_navegadores

¡Esta es una revisión vieja del documento!


Tabla de Contenidos

Eventos en los navegadores

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Eventos en las páginas

Hasta ahora hemos visto muchísimas cosas sobre JavaScript. Con los conocimientos adquiridos podemos crear algoritmos, modificar los contenidos de una página, acceder a cualquier elemento… Pero, todo eso nos sirve de poco si no podemos hacerlo en el momento preciso. Al final, JavaScript dentro de una página web se utiliza casi siempre para interactuar con el usuario. Y precisamente eso es lo que nos proporcionan los eventos. Con lo que veremos en este módulo estaremos cerrando el círculo y llegando a lo que más nos interesa: los usuarios.

pistola

Un documento HTML consta de un amplio conjunto de elementos, como párrafos, enlaces, formularios, tablas o divs. Una vez que se ha cargado una página web comienza la actividad por parte del usuario, que realiza todo tipo de acciones sobre dichos elementos: pulsarlos, seleccionarlos, pasarles por encima… Estas acciones son eventos: acciones que ocurren en la página, y el navegador los transmite a nuestro código por si queremos gestionarlos y responder a ellos con alguna acción. Casi todos los eventos que se generan en una página son provocados por la acción del usuario, con la notable excepción de los eventos que se generan tras la carga de la página y de sus elementos constituyentes.

JavaScript, al igual que la mayoría de los lenguajes de programación para sistemas operativos gráficos, se basa en la programación orientada a eventos.

La estructura clásica de un programa en los antiguos lenguajes no orientados a eventos se denomina Top - Down (Arriba-Abajo) haciendo referencia al hecho de existir normalmente un solo listado de código, el cual se ejecutaba de manera continua siguiendo un orden preestablecido. Era el propio código quien se debía preocupar de detectar las pulsaciones del teclado o el dispositivo de señalización, los cambios en el sistema, etc…

En el paradigma del código orientado a eventos lo que hacemos es escribir muchos pequeños subprogramas, cada uno de ellos ejecutado automáticamente como respuesta a un evento. Así, cuando el usuario pasa el puntero del ratón sobre un objeto, el navegador lanza el evento correspondiente para indicarlo. El programador tiene la posibilidad de escribir código para responder a este evento y ejecutar una (o varias) acción cuando se produzca. No hace falta pues, un programa principal que se esté ejecutando continuamente espiando lo que ocurre para ocuparse de gestionar las diferentes acciones que tengan lugar en el entorno del programa. En nuestro caso, es el propio navegador el que lo hace y notifica estos hechos al programador para facilitar su captura. A las rutinas que un programador escribe para responder a un evento se les llama manejadores de Eventos (en inglés event handlers).

En resumen, debemos quedarnos con la idea de que un manejador de eventos es una función que se llama de manera automática cuando ocurre una determinada acción en una página.

Un poco más de historia para ubicarnos

Ya hemos visto en el módulo sobre el DOM la complicada historia de los navegadores y los hechos que nos han llevado a donde estamos en la actualidad.

Del mismo modo que la unificación de un modelo de objetos fue un proceso complejo, la estandarización de los eventos es incluso más complicada y, de hecho, es donde más diferencias existen entre los navegadores antiguos y modernos. Para ser exactos entre Internet Explorer y los demás navegadores.

Cuando apareció JavaScript con las versiones 3 de Internet Explorer y Netscape se gestionaban unos cuantos (pocos) eventos a través del BOM, y el modelo para gestionarlos era muy similar aunque distinto. Ese sistema sigue vigente hoy en día, al igual que el BOM.

Por otro lado con el DOM Level 2 se intentó crear un estándar para la gestión de eventos, pero sin cubrir todos los tipos posibles. Todos los navegadores modernos implementan este estándar.

El DOM Level 3 incorporó algunos eventos más y complicó un poco más el asunto, si bien, hay un denominador común a todos los navegadores que -con un poco de habilidad- podemos utilizar.

En el DOM Level 4 se incorporaron eventos touch y algunos detalles más.

Gestión de eventos del BOM

La forma más sencilla de gestionar eventos en una página es la que originalmente se incorporó al BOM en los navegadores antiguos. Funciona perfectamente en todos los navegadores y para los casos más comunes y sencillos es suficiente.

En la actualidad no se suele utilizar este modo de gestionar los eventos en aplicaciones serias, básicamente porque limita la gestión a un único manejador y porque implica usar atributos o asignar el método a una propiedad concreta de los nodos, difícil de sustituir luego (aunque veremos cómo hacerlo). No obstante, para cosas rápidas o más sencillas es suficiente y además lo acabarás viendo por ahí, especialmente en aplicaciones poco demandantes como plantillas de gestores de contenido y similares. Así que si quieres puedes saltarte esta parte del BOM, pero no te lo recomiendo porque es muy sencilla y siempre te vendrá bien conocerla.

Supongamos que hemos creado una página que contiene un formulario de entrada de datos con varios campos para rellenar y un botón. Cuando el usuario pulse el botón se generará un evento que indica que se ha realizado dicha acción. A este evento se le llama click o submit, dependiendo del tipo de botón. El hecho de que se haya llevado a cabo esta acción es detectado por el navegador y transmitido a JavaScript para su gestión a través de lo que se conoce como un manejador del evento. El manejador ejecuta un determinado código de JavaScript o llama a una función cuando el evento ocurre, es decir, realiza una tarea como respuesta a ese evento.

En el BOM, los manejadores de eventos se asignan a través de funciones que forman parte de los objetos que generan los eventos, y son llamadas automáticamente por el navegador cuando la acción ocurre. El nombre de estas funciones manejadoras, en el BOM, está formado por el nombre del evento que pretenden controlar precedido por la palabra on, todo en minúsculas. Así, el manejador para un evento click de un botón tendrá por nombre onclick, el manejador para un evento change de un cuadro de texto (cuando cambian sus contenidos) se llamará onchange, y análogamente se haría con los demás, que enseguida detallaremos. De este modo podemos establecer un atributo onclick u onchange en el HTML del elemento, o bien asignar un manejador a las propiedades onclick u onchange en el elemento del DOM correspondiente.

De hecho, ya hemos visto este tipo de eventos en acción, aún sin conocerlos, en alguno de los vídeos y ejemplos prácticos que hemos hecho en módulos anteriores. Es que son muy sencillos y para este tipo de pruebas vienen bien.

Para que un elemento pueda responder a un evento se debe asignar al evento el manejador que se va a utilizar. El manejador puede ser una sucesión más o menos larga de instrucciones JavaScript o, más comúnmente, una función que hayamos definido a tal efecto.

Paramos del siguiente HTML:

<!DOCTYPE html>
<html>
  <head>
    <title></title>
  </head>
 
  <body>
    <form>
      <input type="button" value="Púlsame">
    </form>
  </body>
</html>

Cuando se produce un evento, se dice que se dispara el evento. El código que se encarga de reacciónar al evento, le llamamos manejador del evento.

Tenemos que conocer los eventos disponibles y asignarles manejadores.

En el HTML anterior, lo que queremos es gestionar el evento de que se haga click en el botón. El evento es click y según el BOM, podemos escribirlo como un atributo que será onclick:

(...)
    <form>
      <input type="button" value="Púlsame" onclick="alert('¡Me has pulsado!);">
    </form>
(...)    

Como vemos, dentro del atributo onclick añadimos código JavaScript. Lo normal es que pongamos alguna función que se encargará de gestionar el evento

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <script type="text/javaScript">
      var numVeces = 0;
 
      // Manejador del evento 'click'
      function boton_onClick()
      {
        numVeces++;
        alert("Me has pulsado " + numVeces + " veces.");
      }
    </script>
  </head>
 
  <body>
    <form>
      <input type="button" value="Púlsame" onclick="boton_onClick();">
    </form>
  </body>
</html>

Para intentar separar todo lo posible el JavaScript del HTML, podríamos conseguir lo mismo que antes buscando el botón y luego asociarle el manejador a su evento onclick:

<!DOCTYPE html>
<html>
<head>
   <title></title>
</head>
<script language="JavaScript">
    var numVeces = 0;
 
    function boton_onClick()
    {
      numVeces++;
      alert("Me has pulsado " + numVeces + " veces.");
  }
</script>
 
<body>
    <form>
        <input id="btn1" type="button" value="Púlsame">
    </form>
    <script type="text/javascript">
        var btn = document.getElementById("btn1");
        btn.onclick = boton_onClick;
    </script>
</body>
</html>

Podríamos también usar una función anónima directamente para el evento click:

<!DOCTYPE html>
<html>
<head>
   <title></title>
</head>
 
<body>
    <form>
        <input id="btn1" type="button" value="Púlsame">
    </form>
    <script type="text/javascript">
        var numVeces = 0;
 
        var btn = document.getElementById("btn1");
        btn.onclick = function () {
            numVeces++;
            alert("Me has pulsado " + numVeces + " veces.");
        };
 
    </script>
</body>
</html>

DEMO: desasignar eventos en el BOM

Si queremos dejar de gestionar un evento, podemos asignarle null y así quedará sin ningún manejador de eventos asignado:

<!DOCTYPE html>
<html>
<head>
	<title>Eventos BOM</title>
 
</head>
<body>
	<form>
		<input id="btn1" type="button" value="Púlsame">
		<input id="btn2" type="button" value="Anular" onclick="anularManejador();">
	</form>
</body>
<script type="text/javascript">
	var numVeces = 0;
 
	var btn = document.getElementById("btn1");
	btn.onclick = function (){
		numVeces++;
		alert("Me has pulsado " + numVeces + " veces.");
	};
 
	function anularManejador(){
		btn.onclick = null;
	}
</script>
</html>

DEMO: anular eventos del BOM

Algo que puede ser importante es la posibilidad de poder cancelar un evento, es decir, una vez que se haya producido un evento, evitar que la acción por defecto de ese evento se produzca.

En el siguiente HTML tenemos un enlace que al pulsarlo no hará nada, no llevará a su destino:

<!DOCTYPE html>
<html>
    <head>
       <title></title>
    </head>
 
    <body>
        <a href="http://www.campusmvp.es" id="enlace" onclick="return false;">Ir a campusMVP.es</a>
 
    </body>
</html>

Al devolver false, quedará anulada la acción por defecto que realizaría un click en ese enlace.

Vamos a hacerlo un poco más elaborado. Le preguntaremos al usuario si realmente quiere acceder a la página que le llevaría el enlace:

<!DOCTYPE html>
<html>
    <head>
        <title></title>
    </head>
 
    <body>
 
        <a href="http://www.campusmvp.es" id="enlace">Ir a campusMVP.es</a>
 
        <script type="text/javascript">
            document.getElementById("enlace").onclick = function () {
                var res = window.confirm("¿Quieres ir a campusMVP?");
                return res;
            };
        </script>
    </body>
</html>

Si pulsa Cancelar, devolverá false y entonces el enlace no funcionará.

DEMO: subclasificación de eventos

Veremos cómo solucionar el problema que tenemos en el modelo de eventos del BOM de que solo se pueda asignar por defecto 1 manejador de eventos a un determinado evento.

Partimos del siguiente HTML:

<!DOCTYPE html>
<html>
    <head>
        <title></title>
    </head>
    <body>
 
        <form id="frm1" name="frm1" method="post" action="">
            <input type="text" name="txtNombre" value="" />
            <input type="submit" value="Enviar"/>
        </form>
    </body>
</html>

Queremos que una vez que se ha pulsado el botón de enviar, el usuario no pueda volver a darle al botón. Crearemos un script con una función que se encargue de buscar el elemento submit del formulario:

<!DOCTYPE html>
<html>
    <head>
        <title></title>
    </head>
    <body>
 
        <form id="frm1" name="frm1" method="post" action="">
            <input type="text" name="txtNombre" value="" />
            <input type="submit" value="Enviar"/>
        </form>
 
        <script type="text/javascript">
 
        function submitDisabled() {
            var els = document.forms["frm1"].elements;
            for (var i = 0; i < els.length; i++) {
                if (els[i].type == "submit") {
                    els[i].disabled = true;
                }
            }
 
            // Devuelve 'false' para que no lo envíe y podamos
            // ver que el botón queda deshabilitado
            return false;
        }
 
        var frm = document.getElementById("frm1");
        frm.onsubmit = submitDisabled;
        </script>
    </body>
</html>

Supongamos que tenemos otro formulario y queremos asignarle a un mismo evento dos manejadores, por ejemplo el de deshabilitar el botón de envío y la validación de un campo:

<!DOCTYPE html>
<html>
<head>
   <title></title>
   <script language="javascript" type="text/javascript" src="DeshabilitarSubmit.js"></script>
</head>
<body>
 
    <form id="frm1" name="frm1" method="post" action="">
        <input type="text" name="txtNombre" value="" />
        <input type="submit" value="Enviar"/>
    </form>
 
    <script type="text/javascript">
 
        function validar()
        {
            if (window.frm.txtNombre.value.trim() == "")
            {
                alert("El nombre del usuario es obligatorio");
                return false;
            }
            return true;
        }
 
        var frm = document.getElementById("frm1");
        frm.onsubmit = validar;
    </script>
</body>
</html>

El contenido de DeshabilitarSubmit.js tiene el siguiente contenido:

// Cada 50 milisegundos se llamará a la función 'isPageFullyLoad'
var tmrReady = setInterval(isPageFullyLoaded, 50);
 
// Detectamos si la página si ha cargado correctamente
function isPageFullyLoaded() {
    if (document.readyState == "loaded" || document.readyState == "complete") {
        subclassForms();
        clearInterval(tmrReady);
    }
}
 
// Comprueba si existe un manejador para el evento 'onsubmit'
// En caso de que lo haya, añade la nueva acción para ese evento
function submitDisabled(_form, currSubmit) {
    return function () {
        var mustSubmit = true;
        if (currSubmit != null)
            mustSubmit = currSubmit();
 
        var els = _form.elements;
        for (var i = 0; i < els.length; i++) {
            if (els[i].type == "submit")
                if (mustSubmit)
                    els[i].disabled = true;
            }
            return mustSubmit;
        }
    }
 
// Recorre todos los formularios de una página
// para comprobar si existe un 'onsubmit' y lo deshabilita
function subclassForms() {
    for (var f = 0; f < document.forms.length; f++) {
        var frm = document.forms[f];
        frm.onsubmit = submitDisabled(frm, frm.onsubmit);
    }
}

Eventos según el DOM level 2

Hasta ahora hemos visto la manera clásica (y perfectamente válida todavía) de gestionar los eventos. Aunque está bien para desarrollos sencillos, aparte de sus limitaciones obvias, si nos esmeramos en que nuestro código sea reutilizable en forma de bibliotecas y queremos conseguir una mínima interferencia con el código de los demás, no son el mejor sistema.

Para evitar estos problemas y conseguir un modelo unificado sobre cómo gestionar eventos, el W3C propuso en el año 2000, dentro de DOM Level 2, un sistema general para la captura y ejecución de eventos, el cual vamos a estudiar ahora con sus detalles y salvedades.

Pero antes de verlo necesitamos aclarar otro concepto importante acerca de cómo se capturan los eventos en una página Web.

¿Dónde se captura de verdad un evento?

Esta es una pregunta que tiene bastante más profundidad de la que parece a simple vista. Imaginemos una página con el siguiente código HTML:

<!DOCTYPE html>
<html>
   <head>
      <title> </title>
   </head>
   <body>
      <div>
         <a href="#">Púlsame</a>
 
   </body>
</html>

Como vemos es un simple <div> que contiene a un enlace. Dado que todos los elementos del DOM forman una jerarquía, es decir, están contenidos unos en otros, cuando generamos un evento en uno de ellos (por ejemplo, pulsar el enlace), el evento se propaga por toda la jerarquía del DOM como si fueran las ondas que deja una piedra al tirarla a un estanque:

Flujo-Eventos

Si pulsamos en el enlace, al estar contenido dentro del div es como si pulsásemos también en el div, y a su vez en el cuerpo de la página, el elemento <html> (documentElement) y finalmente en el propio documento que siempre lo contiene a todo.

Es decir, la mayoría de los eventos convergen desde donde se producen hasta el elemento más exterior.

Los anglosajones a este efecto le llaman “event bubbling” o lo que es lo mismo “burbujeo de eventos”, pero a mí me gusta más, en español, utilizar la denominación “convergencia de eventos”, algo que (modestamente 😉) yo mismo acuñé en mi primer libro, en el año 1997, cuando los lenguajes de script comenzaban a ponerse interesantes.

Lo cierto es que es algo más complicado que esto, y un poco antiintuitivo a la vez. En realidad, cuando se produce un evento en un elemento de la página se producen tres fases diferenciadas a la hora de gestionar y lanzar el evento. Estas tres fases se ilustran bien en la figura siguiente:

fase-eventos

  • Fase de captura: en esta fase se comienza a detectar el evento desde el elemento más alto en la jerarquía (el documento), hacia abajo, hasta llegar justo antes del elemento donde realmente se ha generado el evento. En nuestro símil con el estanque es como si las ondas comenzasen a formarse desde fuera hacia adentro, cerrándose sobre el punto de impacto de la piedra en lugar de al contrario. Bastante poco intuitivo, su origen se remonta a la guerra de los navegadores, como veremos enseguida.
  • Fase de objetivo: es el punto 4 del diagrama. Es cuando se detecta el evento en el elemento que lo ha provocado originalmente. Se puede considerar el primer paso de la siguiente fase de convergencia, por eso he pintado la flecha terminando hacia el otro lado.
  • Fase de convergencia: o como dicen los anglosajones “bubbling phase”. Se comienza a detectar el evento de nuevo en todos los elementos de la jerarquía, pero esta vez de abajo a arriba, empezando en el elemento original y llegando hasta la raíz del documento. Es la forma de propagación más lógica e intuitiva en mi opinión.

De este modo, el navegador va verificando en cada elemento de la jerarquía, si se ha definido o no un manejador para el evento que está gestionando, y si es así lo ejecuta, saltando al paso siguiente en caso contrario.

¿Por qué se producen todos estos pasos? ¿Cuál es el sentido de todo esto?

Lo que se pretende conseguir con este recorrido por el DOM es poder capturar los eventos, no en el elemento que los produce, sino en cualquier otro punto intermedio de la jerarquía o incluso en varios puntos diferentes separadamente (podemos gestionar el mismo evento en varios puntos).

De este modo, por ejemplo, se podrían capturar todos los eventos de todos los enlaces contenidos en el <div> de nuestro ejemplo desde un único manejador que los gestione a la vez, en lugar de tener que definir un manejador para cada enlace. O gestionar de manera independiente lo que queremos hacer cuando se pulse un enlace y cuando se pulse el contenedor de éste.

Además, disponiendo de las fases de captura y de convergencia podemos elegir si queremos que se capture antes en los elementos superiores (ahorrando tiempo) o en los inferiores (que como he dicho antes, me parece más intuitivo y coherente).

Dado que además, en general, podremos cancelar el flujo en cualquier punto (subiendo o bajando) podemos evitar que se recoja la acción en puntos inferiores o superiores del proceso, dando lugar a técnicas de optimización avanzadas.

Existe además un detalle histórico a tener en cuenta. Cuando los dos navegadores de los años '90 se decidieron a incorporar el modelo de objetos, Internet Explorer optó por el modelo de convergencia (bubbling) mientras que Netscape lo hizo por el de captura (de arriba a abajo). Así que en ambos navegadores el flujo de lanzamiento de eventos era diferente. El W3C, para no eliminar ninguna de las dos opciones de su modelo y buscar una solución salomónica, decidió meter ambos. De ahí sale todo este embrollo.

Enseguida veremos exactamente cómo funcionan estas fases y cómo entran en juego en los diferentes navegadores.

Gestión de eventos según el DOM level 2

El DOM define dos métodos especializados en la gestión de eventos: addEventListener() y removeEventListener(). Los dos están presentes en todos los nodos del DOM para poder gestionar cualquier evento en cualquier elemento.

Ambos toman tres parámetros obligatorios como argumentos:

  • Nombre del evento a gestionar
  • Referencia a la función que lo gestiona (puedes ser una anónima)
  • Un booleano para indicar en qué fase del flujo de eventos queremos capturarlo. Si le pasamos un valor true entonces los eventos se evalúan en la fase de captura del flujo de eventos visto en la lección anterior. Si se le pasa false (o no se pasa nada, lo más habitual) entonces los eventos se gestionan en la fase de convergencia (de abajo a arriba). Si por algún motivo extraño quisiésemos gestionar el evento en las dos fases del flujo de eventos tendríamos que registrarlo dos veces.

La función addEventListener(), como su propio nombre indica agrega un nuevo manejador de eventos al elemento, colocándolo al final de la cola para ser llamado cuando se produzca.

Esto elimina de un plumazo el problema que teníamos en el BOM sobre qué ocurría si ya existía un manejador de eventos asignado, y que nos llevó a la técnica de subclasificación. En el modelo de eventos del DOM, al contrario que en el de BOM, un mismo evento puede tener registrados varios manejadores. Cuando se produzca el evento, éstos se llamarán en el mismo orden en el que fueron agregados mediante invocaciones de addEventListener.

Ejemplo:

a.addEventListener("click", manejadorEnlace, false);

Importante: con esta función el nombre de los eventos no lleva el prefijo “on” como hasta ahora. Es decir, el evento no es “onclick” sino simplemente “click” y lo mismo ocurre con los demás (“load”, “beforeunload”, etc…). ¡Mucho cuidado con esto!.

Otro detalle a tener en cuenta es que cuando el DOM genera un evento y llama al manejador correspondiente, éste se ejecuta bajo el contexto del elemento que está capturando el evento. Es decir, dentro del manejador la palabra clave this representa el elemento en el que se está capturando el evento, que debido a las fases de flujo y de convergencia no tiene por qué ser el evento en el que se ha producido el evento. Es algo importante que nos puede ahorrar mucho trabajo.

El método removeEventListener funciona exactamente igual y toma los mismos parámetros, sólo que ahora le indicamos qué manejador retiramos y de qué fase. Esto último tiene sentido si pensamos que para un mismo evento podríamos haber añadido varios manejadores en fases distintas del flujo de eventos, así que tenemos que definir ambas cosas también para retirarlos:

a.removeEventListener("click", manejadorEnlace, false);

Como segundo argumento del método addEventListener podemos usar directamente una función anónima si el código es sencillo:

a.addEventListener("click", function(){alert("Hola");}, false);

Pero no podemos hacerlo así, si más tarde pretendemos retirar el manejador del evento. El motivo es que si llamamos a removeEventListener no tendremos una referencia a la función original ya que era anónima. Así que ojo.

DEMO: Manejadores de eventos y convergencia

Veamos cómo funciona la convergencia de eventos, es decir, cómo al producirse un evento en un determinado elemento este se transmite por toda la jerarquía del DOM de ese elemento (de él a sus padres o de sus padres hasta él, depende del modelo que sigamos).

<!DOCTYPE html>
<html>
<head>
    <title> </title>
    <style type="text/css" rel="style">
       #contenedor {
          width:200px;
          height:200px;
          margin: 0 auto;
          padding-top:20px;
          border: 2px solid black;
          text-align:center;
      }
 
      #enlace {
          color: blue;
      }
 
      #mensaje {
          display:block;
          width:400px;
          margin: 10px auto;
          border: 1px dashed black;
      }
  </style>
</head>
<body>
    <div id="contenedor">
      <a id="enlace" href="#">Púlsame</a>
      <br/>
  </div>
  <!-- Para visualizar cómo se están ejecutando los eventos: -->
  <span id="mensaje"></span>
 
  <script type="text/javascript">
    //área de mensaje
    var m = document.getElementById("mensaje");
    var c = document.getElementById("contenedor");
    var manejadorDiv = function()  {
        if (c.style.backgroundColor == "yellow")
            c.style.backgroundColor = "silver";
        else
            c.style.backgroundColor = "yellow";
 
        m.innerHTML += "DIV ";
    };
 
    c.addEventListener("click", manejadorDiv, false);
 
    var a = document.getElementById("enlace");
    var manejadorEnlace = function () {
        if (a.style.backgroundColor == "red")
            a.style.backgroundColor = "transparent";
        else
            a.style.backgroundColor = "red";
 
        m.innerHTML += "A ";
    };
 
    a.addEventListener("click", manejadorEnlace, false);
 
</script>
</body>
</html>

Cada vez que se pulse el enlace, vamos a gestionar el evento click del enlace y también el evento click del contenedor, porque también se detectará este evento en el contenedor. Con la propiedad innerHTML vamos a visualizar si el evento se ha detectado en el <div> o en el <a>.

Cuando ponemos a false el tercer parámetro de addEventListener:

c.addEventListener("click", manejadorDiv, false);

indicamos que el modelo de convergencia de eventos sea de hijo a padre.

Si lo ponemos a true:

c.addEventListener("click", manejadorDiv, true);

indicaríamos que usase el modelo de convergencia de eventos original, el que tenía Netscape. Con este último cambio, cada vez que pulsásemos el enlace, el evento click se detectará primero en el padre (el div contenedor) y luego en el propio enlace.

Modelo unificado de eventos

Nuevamente si quieres obviar esta parte puedes hacerlo en caso de no necesitar dar soporte a Internet Explorer. De todos modos, puede servirte como ejemplo de cómo crear en general código que se degrade paulatinamente en función del soporte existente de características de un navegador. Por otro lado, ten en cuenta que en próximos ejemplos del curso usaremos el manejador universal desarrollado aquí para establecer nuestros eventos. Tú puedes usar addEventListener() sin problema.

Por regla general vamos a utilizar siempre addEventListener() a la hora de gestionar eventos en nuestros desarrollos web.

No obstante, si queremos ofrecer soporte para todos los navegadores, incluso los más antiguos, debido a las diferencias existentes entre los tres modelos para gestión de eventos (BOM, DOM, IE), hay que tener en cuenta muchas cosas diferentes a la hora de declarar un simple evento.

Si se trata de un evento sencillo en una página aislada no hay problema: podemos usar el modelo BOM que si bien es simple y tiene limitaciones, funciona bien siempre y es fácil de utilizar.

Sin embargo, si queremos crear código verdaderamente reutilizable deberíamos elegir siempre el modelo de eventos más apropiado según el navegador que estemos utilizando:

  • Si es una versión moderna de cualquier navegador, entonces el DOM.
  • Si es Internet Explorer 8 o anterior deberíamos usar el modelo IE.
  • Si es un navegador realmente antiguo (hay de todo por ahí accediendo a Internet), entonces el modelo clásico del DOM (o sea, el BOM) será la única opción.

Detectar capacidades del navegador

Tradicionalmente los programadores solían averiguar a través del agente de usuario qué navegador, versión y sistema operativo utilizaban sus usuarios para acceder a las páginas. En función de ello adaptaban su código para usar una u otra funcionalidad. Hoy en día eso no sirve de nada. Aunque averigües esa información no te servirá de mucho ya que las versiones de los navegadores y sus capacidades cambian a gran velocidad (Chrome lanzó 37 versiones en 6 años, de septiembre de 2008 a septiembre de 2014, por ejemplo) y además existe el modo quirks y diversos modos de compatibilidad que hacen que la versión signifique cada vez menos, por no mencionar que directamente muchos navegadores “mienten” en su cadena de agente de usuario y pueden parecer navegadores diferentes.

El agente de usuario es una cadena identificativa que poseen todos los navegadores en la que se refleja el tipo y versión del navegador además de otras informaciones relacionadas, como por ejemplo el sistema operativo bajo el que se ejecuta o las versiones de ciertas bibliotecas auxiliares, como .NET o Java.

El modo quirks es un modo especial de renderizado de algunos navegadores en el que se trata de renderizar la página con el mayor nivel de compatibilidad con navegadores antiguos, y por lo tanto sin soporte para la mayoría de características de HTML5, CSS3 o JavaScript de los navegadores modernos.

Debido a estas particularidades, la estrategia apropiada a seguir para adaptar nuestro código a todas las circunstancias es la de detectar capacidades y no navegadores.

En el caso concreto del modelo de eventos, lo que tenemos que hacer es ir detectando uno a uno los diferentes modelos. Si el primero (DOM) falla ir a por el segundo (IE) y si este tampoco está presente ir a por el tercero (BOM). Con esta estrategia es muy sencillo escribir una clase auxiliar que nos permita trabajar siempre de la manera correcta con los eventos, sea cual sea el navegador:

var EventHandlerHelper =
    {
        addEventListener: function (elt, nomEvnt, handler) {
            if (elt.addEventListener)
                elt.addEventListener(nomEvnt, handler, false);
            else {
                if (elt.attachEvent)
                    elt.attachEvent("on" + nomEvnt, handler);
                else
                    elt["on" + nomEvnt] = handler;
            }
        },
 
        removeEventListener: function (elt, nomEvnt, handler) {
            if (elt.removeEventListener)
                elt.removeEventListener(nomEvnt, handler, false);
            else {
                if (elt.detachEvent)
                    elt.detachEvent("on" + nomEvnt, handler);
                else
                    elt["on" + nomEvnt] = null;
            }
        }
    };

El código está en el archivo “EventHandlerHelper.js” de las descargas de ejemplo.

Lo que hacemos es crear un nuevo objeto llamado EventHandlerHelper en el que definimos dos métodos con el mismo nombre que los que ofrece el DOM: addEventListener y removeEventListener. Con estos sustituiremos a todos los demás, ya que a través de ellos se comprueba cuál es el modelo disponible y se aplica siempre el correcto, sea cual sea el navegador

El método removeEventListener hace el proceso análogo pero inverso.

Con esta nueva clase podemos agregar eventos a cualquier elemento de manera confiable con tan solo escribir algo similar a:

var c = document.getElementById("contenedor");
EventHandlerHelper.addEventListener(c, "click", manejadorDiv);

Un poco más adelante veremos en un vídeo práctico cómo usarlos en un ejemplo. A partir de ahora usaremos siempre este modelo de gestión de eventos para facilitar el trabajo.

Información sobre eventos

Hasta ahora hemos visto algunos eventos muy sencillos, como la pulsación de un botón o el fin de la carga de la página. En éstos, por regla general, no es necesaria información extra, salvo quizá en algunos casos, cuál fue el elemento que lanzó realmente el evento en cuestión (si gestionamos varios desde un mismo manejador).

Lo habitual, sin embargo, es que los eventos necesiten información contextual sobre el proceso que los ha generado. Por ejemplo, si movemos el ratón sobre un elemento podemos necesitar las coordenadas del ratón, o si se pulsa una tecla querremos saber qué tecla ha sido.

Toda esta información se puede conocer a partir de un objeto especial llamado event, que en realidad son muchas clases diferentes que implementan la interfaz Event.

El objeto event en el DOM

En el modelo de eventos sugerido por el DOM todos los manejadores de eventos que declaremos mediante addEventlistener pueden recibir un argumento opcional con una referencia a un objeto event, con información sobre el evento en cuestión.

De este modo podríamos modificar un poco el ejemplo del apartado anterior para hacer algo así:

<!DOCTYPE html>
<html>
<head>
<title> </title>
    <script src="EventHandlerHelper.js"></script>
</head>
<body>
<div id="contenedor">
    <a id="enlace" href="#">Púlsame</a>
    <span id="mensaje"></span>
</div>
 
<script>
    //área de mensaje
    var m = document.getElementById("mensaje");
 
    var manejador = function (event) {
        m.innerHTML += event.currentTarget.tagName + " ";
    };
 
    var c = document.getElementById("contenedor");
    c.addEventListener("click", manejador);
 
    var a = document.getElementById("enlace");
    a.addEventListener("click", manejador);
</script>
</body>
</html>

Fíjate en que ahora hemos usado el mismo manejador para el evento click tanto del div como del enlace que éste contiene (he omitido el cambio de color para simplificarlo). Además este manejador ahora toma como argumento una referencia a un objeto de nombre event que utilizamos para averiguar en qué elemento se está capturando el evento en cada momento, usando la propiedad currentTarget de éste.

Este objeto event ofrece mucha información sobre el evento actual, la cual dependerá además del tipo de evento que se esté produciendo. Los miembros base de un evento en el DOM se pueden ver en la tabla siguiente y puedes consultar la referencia completa en la MDN:

Nombre Tipo Descripción
bubbles booleano Indica si el evento se transmite por la jerarquía del DOM o no. No todos los eventos convergen. Por ejemplo, el evento “focus” sólo se lanza en el evento que toma el foco, y no se transmite a los demás de la jerarquía, es decir, no hay fase de captura ni de convergencia. Se debe consultar para cada evento.
cancelable booleano Nos sirve para averiguar si la acción por defecto del evento se puede cancelar. Se debe consultar para cada evento.
currentTarget elemento Una referencia al elemento del DOM que está capturando el evento actualmente. OJO: no tiene que ser el mismo elemento que lo ha lanzado, ya que podemos capturarlo en cualquier nivel de la jerarquía, como ya sabemos. En el modelo del DOM la palabra clave this siempre apunta al mismo elemento que esta propiedad, así que es equivalente escribir event.currentTarget.tagName que this.tagName
detail numérico Proporciona información extra sobre el evento. Depende del tipo de evento. Por ejemplo, en un clic nos dice el número de veces que se ha pulsado, en eventos de rueda de ratón la distancia que se ha girado la rueda… No se suele utilizar demasiado, pero siempre está presente en eventos de interfaz de usuario (UIEvent) o en eventos personalizados.
eventPhase numérico Vale 1 si el evento se ha lanzado en la fase de captura, 2 en el elemento que lo ha generado, y 3 si es en la fase de convergencia.
preventDefault() función Si la propiedad cancelable es true se puede llamar a este método para cancelar la acción por defecto. Es muy interesante para, por ejemplo, evitar que se navegue a través de un enlace y éste actúe solamente como un botón, o para evitar que se envíe un formulario, aunque en estos casos, devolver false, o establecer la propiedad returnValue del evento como false, tiene el mismo efecto. La propiedad defaultPrevented del evento sirve para comprobar si se ha cancelado la acción por defecto o no con este método.
stopPropagation() función Si la propiedad bubbles es true, entonces llamando a este evento se cancela la transmisión a la jerarquía del DOM, es decir, no sigue la fase de captura ni el “burbujeo” de la fase de convergencia y no se detecta en los siguientes nodos del DOM. Por ejemplo, si capturamos el clic en un enlace y en su div contenedor, pero no queremos capturarlo en ambos a la vez, podemos cancelar la propagación en el manejador del enlace y así no se detectará también en el div.
stop​Immediate​Propagation() function Como sabemos, cuando un mismo elemento tiene más de un manejador asociado para el mismo evento, éstos se ejecutan en el orden en el que fueron especificados. Si desde alguno de ellos llamamos a este método, los demás manejadores posteriores que no se hayan ejecutado todavía, no se ejecutarán, pues cancela toda llamada posterior.
target elemento El elemento de la página que realmente ha producido el evento. En nuestro ejemplo, si pulsamos el enlace pero capturamos en el div, esta propiedad apuntaría al enlace que es el que ha provocado el evento. Es una de las propiedades más importantes de un evento.
type texto El tipo de evento que se ha lanzado. Es la misma cadena que usamos para declarar el evento: “click”, “load”, focus“… Nos sirve para distinguir el tipo de evento cuando varios comparten el mismo manejador.
view objeto Es un objeto de tipo AbstractView definido por la W3C y que es de donde derivan todas las posibles vistas del documento (se refiere a documentos XML genéricos, que pueden visualizarse de modo diferente según su XSLT). En este caso se refiere a la vista actual del documento, lo cual implica la página actual. Es decir, en la práctica con ella lo que obtenemos es una referencia al objeto window actual. Más información aquí. Solo se ofrece en eventos de interfaz de usuario, o sea, de tipo UIEvent.

Todas las propiedades son de solo lectura, es decir, podemos leer su valor y no cambiarlo, lo cual es lógico pues simplemente nos proporcionan información.

Además de las propiedades que hemos visto en la tabla anterior, el objeto event posee otras que solamente aparecen cuando tienen sentido para el evento que estamos capturando y nos informan, por ejemplo, de las teclas que se han pulsado en el teclado, o de las las coordenadas del ratón, las vueltas de la rueda del ratón, etc… Más adelante veremos los eventos más importantes y conoceremos estas propiedades particulares.

El objeto event en Internet Explorer 8 o anterior

Contenido opcional: esto es historia antigua y, como todo el contenido relativo a IE y versiones antiguas, si no quieres hacer esta lección puedes dejarla. De todos modos lo hemos dejado porque puedes encontrarte de todo por ahí y, entonces, te alegrarás de conocerlo 😉

Como ya hemos dicho antes, a partir de la versión 9 de Internet Explorer soporta el modelo estándar del W3C para eventos (el DOM), por lo que no hay problema en ese caso. Sin embargo, en IE8 y anteriores, como ya hemos estudiado, el modelo cambia bastante.

Lo que más cambia es la forma de acceder al objeto event, ya que hay dos casos bien diferenciados:

  1. En el caso de que usemos attachEvent para adjuntar los eventos, el objeto event se pasa al manejador como primer argumento, exactamente igual a como acabamos de ver para el DOM.
  2. Si usamos el estilo del BOM para asignar los eventos, entonces los manejadores no reciben información alguna, y debemos acceder a la información sobre el evento a través de la propiedad event de la ventana (window.event).

El resto de los navegadores, excepto Firefox, soportan también acceder al objeto event desde la ventana en este caso, para tener compatibilidad con este modo de actuar.

Incluso en el caso 1, aunque el funcionamiento es igual al del DOM, el problema es que las propiedades del objeto event de Internet Explorer tienen nombres diferentes a las que tienen en el DOM. Eso sí, aunque son menos, la funcionalidad que se puede conseguir es muy parecida.

En la tabla siguiente podemos ver las propiedades base de un evento de Internet Explorer:

Propiedad Tipo Descripción
cancelBubble booleano Permite averiguar o fijar si el evento convergerá o no. Como en IE no hay fase de captura no tiene sentido la propiedad eventPhase. Si queremos hacer, lo equivalente al método stopPropagation del DOM sólo tenemos que asignar true a esta propiedad.
returnvalue booleano Es el valor de retorno que se enviará como resultado del evento. Por defecto siempre vale true. Si le ponemos false como valor a esta propiedad es equivalente a llamar a preventDefault() en el DOM o a devolver false en el manejador: cancela la acción por defecto.
srcElement elemento El elemento que ha provocado el evento. Es igual a la propiedad target en el evento del DOM.
type texto Es idéntica a la propiedad del mismo nombre en el evento del DOM.

La única dificultad real que existe en este modelo es que no hay forma de averiguar cuál es el elemento actual que está gestionando el evento, es decir, no hay equivalente a la propiedad currentTarget del DOM. Más allá de la diferencia de nombres y comportamientos, éste es el principal motivo para tratar de evitar dicho modelo (si podemos).

Una última diferencia importante es que en versiones antiguas de Internet Explorer, en función de cómo declaremos el evento tendremos un valor diferente de ámbito para el manejador, es decir, la palabra clave this tendrá un valor diferente dentro del manejador. Así, si asignamos el evento usando el BOM (ejemplo elto.onclick=manejador;) entonces this apuntará al objeto que ha lanzado el evento, es decir lo mismo que la propiedad srcElement del evento. Sin embargo, si lo adjuntamos con attachEvent entonces this apuntará al objeto global (la ventana).

Un objeto event unificado

Contenido opcional: como todo el contenido relativo a IE y versiones antiguas, si no quieres hacer esta lección puedes dejarla.

Para tratar de paliar la dificultad de lidiar con todos los detalles de trabajar con los eventos y asegurarnos de que éstos van a funcionar bien en todos los navegadores, vamos a ampliar la funcionalidad de nuestro objeto EventHandlerHelper para que añada un buen soporte para los objetos event también.

Si le añadimos el siguiente método:

fixEvent: function (event) {
    var evt = event ? event : window.event;
    if (!evt.bubbles) evt.bubbles = evt.cancelBubble;
    if (!evt.cancelable) evt.cancelable = evt.returnValue;
    if (!evt.currentTarget) evt.currentTarget = evt.srcElement;
    if (!evt.preventDefault) evt.preventDefault = function () { evt.returnValue = false; };
    if (!evt.stopPropagation) evt.stopPropagation = function () { evt.cancelBubble = true; };
    if (!evt.target) evt.target = evt.srcElement;
    if (!evt.view) evt.view = window;
    if (!evt.relatedTarget) evt.relatedTarget = evt.fromElement ? evt.fromElement : evt.toElement;
    if (!evt.which) evt.which = evt.keyCode != 0 ? evt.keyCode: evt.charCode;
    if (!evt.key) evt.key = String.fromCharCode(evt.which);
    return evt;
    }

Lo que hacemos es obtener el objeto evento desde donde esté disponible, del argumento en caso de que exista o del objeto window si no es así. Una vez que lo tenemos, comprobamos si tiene las propiedades y métodos del DOM, añadiéndoselas desde las correspondientes de Internet Explorer si no es así.

Las únicas propiedades que quedan fuera son eventPhase y detail, por imposibilidad de obtenerlas. La propiedad currentTarget se define para que no falle pero no nos es muy útil ya que en el modelo de Internet Explorer siempre apuntará al elemento que lanza el evento, no al que lo captura, por lo que no conviene usarla (Por ejemplo jQuery la incluye también –le llama currentTarget- aunque tiene la misma limitación).

A partir de ahora, si necesitamos código compatible con todos los navegadores, bastará con declarar todos los manejadores al estilo DOM (con el evento como argumento) y llamar a esta función para asegurarnos de tener un objeto event compatible con el DOM en gran medida. Por ejemplo, el código anterior quedaría para Internet Explorer así:

var m = document.getElementById("mensaje");
 
var manejador = function (event) {
    event = EventHandlerHelper.fixEvent(event);
    m.innerHTML += event.srcElement.tagName + " ";
    event.stopPropagation();
    event.preventDefault();
};
 
var c = document.getElementById("contenedor");
c.attachEvent("onclick", manejador);
 
var a = document.getElementById("enlace");
a.attachEvent("onclick", manejador);

En este fragmento (que sólo funcionará en IE) adjuntamos sendos manejadores usando el método específico de IE llamado attachEvent. En el evento llamamos a fixEvent y obtenemos un objeto evento que podemos usar de la manera habitual, como si fuera un evento del DOM, ya que le hemos añadido las propiedades y métodos que le faltaban. He metido llamadas a stopPropagation y preventDefault para verificar que funcionan bien.

El método fixEvent nos servirá siempre, sea cual sea el modelo de eventos y el navegador utilizado. En los ejemplos posteriores de este módulo lo utilizaremos siempre, pero tú utiliza simplemente addEventListener y el modelo del DOM si lo prefieres.

DEMO: Modelo unificado, funcionamiento y uso

Veremos el mismo que ejemplo que en apartado DEMO: Manejadores de eventos y convergencia, pero adaptado a este modelo unificado.

<!DOCTYPE html>
<html>
    <head>
        <title> </title>
 
        <script type="text/javascript" src="EventHandlerHelper.js"></script>
 
        <style type="text/css" rel="style">
           #contenedor {
              width:200px;
              height:200px;
              margin: 0 auto;
              padding-top:20px;
              border: 2px solid black;
              text-align:center;
          }
 
          #enlace {
              color: blue;
          }
 
          #mensaje {
              display:block;
              width:400px;
              margin: 10px auto;
              border: 1px dashed black;
          }
      </style>
    </head>
    <body>
        <div id="contenedor">
          <a id="enlace" href="#">Púlsame</a>
          <br/>
      </div>
      <span id="mensaje"></span>
 
        <script type="text/javascript">
            //área de mensaje
            var m = document.getElementById("mensaje");
            var c = document.getElementById("contenedor");
            var manejadorDiv = function(event)  {
                EventHandlerHelper.fixEvent(event);
                if (event.currentTarget.style.backgroundColor == "yellow")
                    event.currentTarget.style.backgroundColor = "silver";
                else
                    event.currentTarget.style.backgroundColor = "yellow";
                m.innerHTML += "DIV ";
            };
 
            EventHandlerHelper.addEventListener(c, "click", manejadorDiv);
 
            var a = document.getElementById("enlace");
            var manejadorEnlace = function (event) {
        		/* En el vídeo del curso se omite la asignación a 'event', lo cual funcionará en el 99,9% de los casos pero en versiones muy antiguas de IE en las que event será nulo originalmente es necesario asignarlo de nuevo para que obtenga lo devuelto por fixEvent ¡OJO! */
                event = EventHandlerHelper.fixEvent(event);
                if (event.currentTarget.style.backgroundColor == "red")
                    event.currentTarget.style.backgroundColor = "transparent";
                else
                    event.currentTarget.style.backgroundColor = "red";
                m.innerHTML += "A ";
            };
 
            EventHandlerHelper.addEventListener(a, "click", manejadorEnlace);
        </script>
    </body>
</html>

Eventos más habituales

Hasta llegar hasta aquí, hemos recorrido un largo camino para poder entender cómo funcionan los eventos en los navegadores y cómo podemos sacarles partido desde JavaScript. Ahora nos queda lo más fácil y entretenido: saber qué eventos tenemos a nuestra disposición y qué particularidades tienen para poder gestionarlos.

Hay decenas de eventos posibles y varían de un elemento a otro, e incluso de un navegador a otro, ya que no cualquier evento está soportado en todos ellos. Sin embargo, hay un subconjunto que está ampliamente soportado y que además es el que usaremos de manera más habitual. Será el que repasaremos en esta parte del módulo.

Mi recomendación es tener siempre a mano una buena referencia on-line que nos informe de las propiedades y particularidades de cada evento. Existen infinidad de eventos, algunos atados a APIs concretas y otros personalizados, pero también podemos definir una lista de eventos estándar que se definen en las especificaciones web, y que son comunes a todos los navegadores.

Además, otra página interesante para tener como referencia, aunque ya tiene unos años, es Dottoro. Esta página nos permite localizar cualquier elemento, bien mediante la búsqueda integrada, bien recurriendo al índice, y nos proporciona información detallada y ejemplos de cada uno, indicando además los atributos y propiedades de cada uno, los eventos que soporta… y además muestra mediante iconos el soporte que ofrece cada navegador para el miembro consultado (si bien no la actualizan a menudo y puede que haya algún dato erróneo, pero aún así merece mucho la pena como primer filtro):

Dottoro - Buscar elementos en índice

Las principales herramientas de escritura de código ofrecen también ayuda contextual mientras escribimos, por lo que nos facilitarán mucho asignar eventos, propiedades y saber qué parámetros utilizar. Aprovecha su potencia.

A continuación vamos a repasar de manera somera los eventos más importantes comunes a todos los navegadores, aunque algunos ya los hemos ido viendo mientras avanzábamos en la explicación de los modelos de eventos.

Eventos de ciclo de vida de la página

Desde que se solicita un URL, el navegador descarga sus contenidos, los procesa y los visualiza, ocurren varias cosas y se producen diversos cambios de estado en la página. El navegador nos proporciona eventos que lanza en ciertos puntos de su ciclo de vida para poder llevar a cabo tareas de inicialización o responder a esos cambios. Los principales son:

  • load: nos informa de cuándo la página ha terminado de cargar y de ser procesada. Esto incluye no solamente la carga y el procesado del HTML sino también la carga de cualquier recurso externo que haya, incluidas las imágenes. Ya lo hemos mencionado en algún ejemplo. Se define en el nivel 3 del DOM. Es interesante para saber cuándo todo el procesamiento inicial de la misma por parte del navegador ha terminado y contrasta con el evento DOMContentLoaded que veremos en detalle en breve que salta antes, en cuanto el DOM ha sido procesado. Las imágenes y los marcos también disponen de este evento. Se consideran eventos de interfaz de usuario y no hay que confundirlo con el evento load del objeto XHR para AJAX.
  • unload: se produce cuando el navegador descarga el documento actual, bien porque cambiamos de página o porque el usuario cierra la ventana. Si hay marcos se produce antes en la ventana “madre” que en los marcos que la contienen. Está definida en el nivel 3 del DOM. Dentro de este evento no se pueden mostrar ventanas con alert o similar ni ejecutar procesos largos o se cancelarán por parte del navegador. Por ejemplo, si necesitamos hacer una llamada al servidor durante la descarga de la página, desde luego no puede hacerse con una llamada AJAX síncrona (eso no se recomienda nunca en ningún lado) y es mejor utilizar la API de sendBeacon() para hacerlo, que realiza la llamada y no espera respuesta. No es cancelable ni converge.
  • beforeunload: este evento es mucho más interesante que el anterior ya que nos notifica antes de que se vaya a descargar la página y nos da la oportunidad de evitarlo. No está soportado en versiones viejas de Opera. Enseguida lo veremos con más detalle. Está definido en HTML5. Es cancelable por el usuario, como veremos enseguida, impidiendo que la página se descargue si es necesario.
  • pageshow: este evento se lanza cuando la página actual se hace visible debido a acciones relacionadas con la navegación, ya que es un evento de transición de página definido en HTML5. Es decir, se notifica al cargar inicialmente la página (después del evento load), al navegar a ella desde otra página o al regresar a la misma mediante el uso del historial. Dispone de una propiedad para el evento llamada persisted que indica mediante un booleano si la página va a ser almacenada o no en una caché para mostrarla de nuevo (lo habitual cuando navegamos a otra y luego regresamos: la página no carga de nuevo al darle para atrás al navegador, solo se muestra y por lo tanto no salta el evento load sino pageshow.
  • pagehide: ídem que la anterior, pero para cuando la página deja de ser visible debido a navegación. Salta antes que beforeunload y que unload.
  • hashchange: el navegador notifica este evento para la ventana actual cuando cambia el valor que hay detrás del símbolo almohadilla (#) en el URL de la página. Es sumamente importante porque es la base en la que se apoyan las modernas aplicaciones de tipo “Single Page”.
  • readyStateChange: este evento se notifica cada vez que hay un cambio en el estado de carga de la página. Lo detallaremos en la próxima lección.

Los navegadores móviles, por el propio funcionamiento de estos dispositivos no siempre notifican de los eventos pagehide, beforeunload o unload. En estos navegadores el usuario puede mandar el navegador a segundo plano simplemente pulsando el botón de inicio, y luego el sistema operativo puede reclamar la memoria utilizada “matando” el navegador sin más, por lo que no saltará ninguno de los eventos. Incluso algunos navegadores (por ejemplo el que viene nativamente con los móviles Xioami o el conocido navegador móvil UCBrowser) directamente no los implementan y no saltan jamás. Es decir, en móviles debemos considerar estos eventos como una excepción cuando saltan, y no una norma. La única forma de determinar en estos casos si una página se oculta o se muestra es usando una API especial de HTML5: la Visibility API. Tenlo en cuenta para estos casos.

Son todos eventos de uso sencillo y directo que se suelen capturar en la ventana actual o en la ventana de los marcos internos. Veamos unos cuantos ejemplos prácticos de uso.

Ejemplo: Detectando que la página está lista

Como hemos visto, el objeto window dispone del evento load que se llama cuando la página ha sido cargada por completo. Que esté completamente cargada quiere decir que el HTML ha sido procesado, el DOM se ha creado y todos los recursos externos (hojas de estilo, scripts, imágenes…) están cargados, procesados y visualizados. Podemos responder a este evento asignándolo como un atributo del nodo <body> (como ya hemos comentado en un vídeo anterior) o bien usando JavaScript en el propio objeto window:

window.onload = mi_window_onload;

Aunque la forma más habitual de hacerlo es mediante addEventListener, así:

window.addEventListener("load", mi_window_onload);

El evento es muy útil y es fácil de usar y nos asegura que cuando se llame a su manejador, todos los elementos de la página estarán cargados y listos para su uso, incluyendo los que hay que cargar en una segunda fase tras el procesamiento del HTML, como imágenes y hojas de estilo.

Sin embargo, en algunas ocasiones nos hará falta detectar una fase anterior a la carga en el ciclo de vida de la página: cuando la jerarquía de objetos del DOM está lista para ser utilizada desde nuestro código JavaScript. Esto implica que se ha interpretado todo el HTML, se han cargado los scripts y por lo tanto se puede manipular el contenido del documento sin problemas. En este momento sin embargo puede que todavía no se hayan cargado las imágenes y otros elementos, pero ya podemos manipular sin problemas el DOM.

Se trata de un estado anterior al de la carga completa de la página, y es el momento ideal para asignar eventos, puesto que hay ciertos eventos que se lanzarán incluso antes que el load de la página. Por ejemplo, las imágenes lanzan su propio evento load cuando cargan su contenido, y si queremos responder a éste entonces debemos hacerlo antes del load de la página completa.

La biblioteca jQuery nos ofrece el método ready() para detectar esta circunstancia, facilitándonos mucho la vida, pues basta con definir una función dentro de éste para que sea llamada en cuanto la página esté lista para ser manipulada.

En este apartado veremos cómo conseguir esta misma funcionalidad directamente con JavaScript y para todos los navegadores, sin utilizar ninguna biblioteca externa.

Todos los navegadores actuales disponen de una propiedad denominada readyState, del objeto document. Ésta nos devuelve una cadena de texto que indica el estado actual en el ciclo de carga de la página. Los valores que puede tener son los siguientes, ordenados por el momento en el que se producen dentro del ciclo de vida de la página:

Estado Descripción
loading Se están empezando a cargar los datos de la página, e interpretando el HTML
interactive El usuario ya puede interactuar con los elementos de la página aunque todavía no están cargados todos (por ejemplo, faltan las imágenes).
complete Se ha terminado de cargar la página completamente

A partir de esta propiedad, por lo tanto, es sencillo determinar en qué estado se encuentra nuestra página, y si está en estado interactive o superior, entonces sabemos que podemos ya interactuar con el DOM desde nuestros scripts. Pero: ¿cómo detectamos el cambio de estado?

Existe un evento estándar muy útil llamado onreadystatechange. Basta con interceptarlo y saltará en cada uno de los cambios de estado (o sea, dos veces: de loading a interactive y de éste a complete), por lo que es fácil determinar mediante document.readyState en qué estado nos encontramos:

document.addEventListener('readystatechange', function() {
    console.log(document.readyState);
});

Y de este modo, cuando llegue a interactivo, podemos determinar que al menos el DOM ya está cargado. Fíjate en que este evento se intercepta en el documento, no en la ventana.

De todos modos existe un evento más apropiado denominado DOMContentLoaded, que nos permite determinar esto de manera automática. Basta con interceptarlo para saber cuándo está la página lista para poder interactuar con el DOM:

window.addEventListener('DOMContentLoaded', function() {
    console.log('El DOM ya está interactivo en este momento!!');
});

Este evento salta para la ventana, no para el documento.

DEMO: Salida de la página

Veremos cómo se gestionan los eventos de ciclo de vide de la página, en concreto los que permiten detectar cuándo una página se descarga.

Partimos de este HTML:

<!DOCTYPE html>
<html>
    <head>
        <title>Prueba de abandono de página</title>
    </head> 
    <body>
        <h1>Ejemplo de abandono de página</h1>
 
        <br/><br/>
 
        <a href="http://www.campusmvp.com">Ir a otra página</a>
 
    </body>
</html>

Para detectar que la página se está descargando de memoria para dar paso a una nueva carga de sí misma o de otra página, podemos usar el evento unload:

<!DOCTYPE html>
<html>
    <head>
        <title>Prueba de abandono de página</title>
    </head>
    <body onunload="alert('Adiós!');">
        <h1>Ejemplo de abandono de página</h1>
 
        <br/><br/>
 
        <a href="http://www.campusmvp.com">Ir a otra página</a>
 
    </body>
</html>

Con ese unload lograremos que cada vez que se cargue actualice la página o pinchemos en el enlace para salir de ella, nos saltará ese alert.

Esto tiene algunas limitaciones como que si cerramos el navegador, no salta ese alert.

Existe un evento específico llamada onbeforeunload que se dispara justo antes de que se vaya a descargar la página, permitiéndonos reaccionar antes de que la página se descarga y también preguntar al usuario si quiere descargar la página. Esto se suele ver en aplicaciones web de correo para que no cerremos la ventana antes de enviar un e-mail, por ejemplo. También cuando tenemos algún cambio pendiente de confirmar.

<!DOCTYPE html>
<html>
    <head>
        <title>Prueba de abandono de página</title>
        <script type="text/javascript">
            var bPreguntar = true;
 
            window.onbeforeunload = function () {
		if (bPreguntar)
                    return "¿Seguro que quieres salir?";
            }
        </script>        
    </head> 
    <body>
        <h1>Ejemplo de abandono de página</h1>
        <p>Por defecto preguntará hasta que pulses el botón de "No preguntar"</p>
        <input type="button" value="Preguntar" onclick="bPreguntar = true;">&nbsp;&nbsp; 
        <input type="button" value="No preguntar" onclick="bPreguntar = false;">
        <br/><br/>
 
        <a href="http://www.campusmvp.com">Ir a otra página</a>
 
    </body>
</html>

Eventos del ratón

Esta es la manera más obvia de interactuar con el usuario. Ya hemos visto el más importante, click, que no tiene mucho misterio, pero hay más. A continuación tienes los más importantes:

  • click: se produce cuando el usuario pulsa un elemento con el botón principal del ratón (generalmente el izquierdo). Es importante señalar que si un elemento tiene el foco y el usuario pulsa la tecla ENTER entonces salta también este evento, algo importante para usuarios con discapacidad.
  • contextmenu: salta cuando el usuario pulsa con el botón secundario del ratón (generalmente el derecho) y aparece el menú contextual del elemento.
  • dblclick: detecta el hacer doble clic sobre un elemento.
  • mousedown: se lanza cuando el usuario pulsa un botón del ratón sobre algún elemento, pero antes de que lo suelte. Es decir, es como medio clic.
  • mouseup: es el complementario del anterior. Salta cuando el usuario suelta el botón del ratón tras haberlo pulsado. Una secuencia completa de mousedown + mouseup genera un click si no se cancela el evento antes.
  • mousemove: se genera cuando el cursor se mueve por encima de un elemento.
  • mouseover: se lanza cuando el cursor entra en el área de un elemento. Por ejemplo, si tenemos un div en forma de cuadrado en la página, cuando el cursor entra sobre el cuadrado se lanza este evento y cuando ya está sobre éste y se mueve se generaría el mousemove.
  • mouseenter: es igual que el anterior pero difiere en que no converge, es decir, no se notifica en otros niveles del DOM. Sin embargo se notifica para todos los elementos que contienen a otro a medida que se sale de la superficie de cada uno de los interiores hacia el exterior. Esto puede dar lugar a muchos eventos en caso de que se capture en todos ellos. En general se recomienda mejor utilizar el anterior.
  • mouseout: es el contrario del anterior y se da cuando el cursor sale de encima de un elemento. Cuando el ratón se mueve de un elemento a otro, en el que abandona salta el mouseout y en el que entra lanza un mouseover.
  • mouseleave: como el anterior pero sin converger. Se recomienda utilizar mouseout.
  • scroll: este evento salta cuando la ventana sobre la que se captura mueve su contenido internamente. Por ejemplo en una ventana o en un iframe cuyos contenidos excedan sus dimensiones y tenga por lo tanto barras de scroll. Se puede leer la cantidad de píxeles que se ha movido el contenido usando las propiedades scrollX y scrollY del objeto evento. Más adelante hacemos un ejemplo muy completo sobre cómo detectar la visibilidad de un objeto dentro de una página.
  • wheel: permite conocer cuándo el usuario mueve la típica rueda que hay en el medio de los botones de la mayoría de ratones del mercado. Las propiedades deltaX, deltaY y deltaZ del evento nos indican las cantidades que se ha movido la rueda respecto a la posición anterior. Estas unidades pueden ser píxeles, líneas o páginas, y podemos averiguarlo mediante la propiedad deltaMode del evento, que puede tomar los valores 0, 1 o 2 respectivamente para indicarlo. Si el número es negativo es que la rueda se ha movido hacia atrás. En versiones antiguas de Internet Explorer se definía un método similar llamado OnMouseWheel y que fue el pionero de este tipo de eventos.
  • select: salta cuando se ha seleccionado algún contenido en la página, con el ratón, con los dedos (en pantallas táctiles) o con cualquier otro método. Es posible obtener el contenido seleccionado usando las propiedades selectionStart y selectionEnd del elemento seleccionado. Puedes ver un ejemplo aquí.

Existen algunos eventos de ratón adicionales relacionados con las operaciones de arrastrar y soltar (drag, drop, dragstart, dragend, dragenter, dragleave y dragover) pero no los estudiaremos en este curso por estar más relacionados con la API de HTML5 específica para arrastrar y soltar.

Un evento click siempre va precedido por un evento mousedown y otro mouseup. Y un dblclick duplica ese patrón, ya que obviamente para conseguir un doble clic son necesarios dos clics y para cada clic es necesario bajar y subir el botón.

Todos los eventos del ratón se propagan por el DOM pero todos pueden cancelar esta propagación (con stopPropagation) y todos pueden cancelar su acción por defecto (con preventDefault).

Hay cierta información que podemos necesitar a la hora de trabajar con los eventos de ratón, como las coordenadas en las que se produjo el evento o si había algún botón pulsado. Todas ellas las podemos obtener a partir de propiedades del objeto evento, en concreto:

  • clientX, clientY: son las propiedades horizontal y vertical que tenía el ratón dentro del área cliente del navegador (el viewport) cuando se produjo el evento. Están referidas a la esquina superior izquierda de esta área. También existen unos alias más abreviados que son simplemente x e y, por lo que podemos escribir tan solo event.x o event.y y obtenemos lo mismo. Aunque las propiedades originales son las otras y estas no aparecen en el estándar, si bien funcionan incluso con IE6.
  • offsetX, offsetY: las coordenadas del ratón respecto al borde del elemento sobre el que se detecta el evento, considerando el relleno también (es decir, considera el padding pero no el borde). Útil si solo nos interesan las coordenadas dentro del elemento.
  • pageX, pageY: las coordenadas del ratón respecto al total de la página, no del área visible. Si no hay scroll entonces coinciden con clientX y clientY, pero en general no será así y usaremos estas últimas.
  • screenX, screenY: las mismas coordenadas pero esta vez referidas a la pantalla completa del ordenador, es decir, a la esquina superior izquierda de la pantalla.
  • movementX, movementY: coordenadas relativas a la última ve que se lanzó el evento mouseMove. Poca aplicación práctica, aunque puede ser útil para casos especiales.
  • button: esta propiedad nos indica qué botón estaba pulsado durante los eventos mousedown o mouseup. Su valor es 0 para el botón izquierdo, 1 para el del medio y 2 para el derecho o secundario. En ratones especiales que tienen botones para otros propósitos (como navegar hacia delante o atrás con el navegador) puede devolver también los valores 3 y 4 para dichos botones.
  • Shiftkey, ctrlKey, altKey, metaKey: es el estado de pulsación (true si están pulsadas), cuando se produjo el evento, de las distintas teclas de control que tienen los ordenadores: la tecla de mayúsculas, la de control, la ALT y la tecla meta (presente en algunos sistema UNIX, no soportada por IE). Útil si queremos combinar el evento del ratón con tener alguna de estas teclas pulsada.
  • relatedTarget: esta propiedad solo se establece durante los eventos mouseout y mouseover (y mouseenter y mouseleave que son casi equivalentes) y sirve para indicar qué elemento sigue o precede respectivamente al elemento actual sobre el que actúa el evento. Es decir, si muevo el cursor del elemento A al B, salta el elemento mouseout en A con relatedTarget apuntando a B y acto seguido salta mouseover en B con relatedTarget apuntando a A. De este modo es fácil conocer el camino que ha seguido el ratón. En el modelo de eventos de Internet Explorer no existe y tendremos que recurrir a dos propiedades relacionadas llamadas toElement y fromElement. Se puede añadir una línea más a nuestra función de compatibilidad (creada anteriormente, fixEvent) para solucionarlo, así:
if (!evt.relatedTarget) evt.relatedTarget = evt.fromElement ? evt.fromElement : evt.toElement;

DEMO: Perseguir al ratón

La siguiente página HTML (con su código JavaScript) dibujará un rectángulo y sus coordenadas que irán persiguiendo el puntero del ratón.

<!DOCTYPE html>
<html>
    <head>
        <title> </title>
        <style type="text/css">
        .flotante
        {
            position:absolute;
            border: 1px solid red; 
            width:150px; 
            padding:5px;
            font-size: small;
            font-weight:bold;
            text-align: center;
            background-color:yellow;
            visibility: hidden;
        }
        </style>
        <script type="text/javascript" src="EventHandlerHelper.js"></script>
        <script type="text/javascript">
        // Referencia al mensaje flotante con las coordenadas
	var mf = null;
 
        // Manejador del evento
        function moveDiv(event)
        {
            event = EventHandlerHelper.fixEvent(event);
            // Obtendremos el 'div' flotante cada vez que se mueva el ratón
	    if (mf == null)
		mf = document.getElementById("mensajeFlotante");
 
            mf.textContent = "(" + event.clientX + ", " + event.clientY + ")";
	    mf.style.left = event.clientX + 10 + "px";
            mf.style.top = event.clientY + "px";
            mf.style.visibility = "visible";
        }
 
        // Añadiremos el manejador del evento al propio evento, que para nuestro
        // ejemplo es 'mousemove'
        function initialize()
        {
            EventHandlerHelper.addEventListener(document, "mousemove", moveDiv);
        }
 
        window.onload = initialize;
    </script>
    </head>
    <body>
        <div id="mensajeFlotante" class="flotante">¡Por qué me persigues!</div>
    </body>
</html>

El archivo EventHandlerHelper.js contiene funciones para compatibilidad con navegadores antiguos:

var EventHandlerHelper =
    {
        addEventListener: function (elt, nomEvnt, handler) {
            if (elt.addEventListener)
                elt.addEventListener(nomEvnt, handler, false);
            else {
                if (elt.attachEvent)
                    elt.attachEvent("on" + nomEvnt, handler);
                else
                    elt["on" + nomEvnt] = handler;
            }
        },
 
        removeEventListener: function (elt, nomEvnt, handler) {
            if (elt.removeEventListener)
                elt.removeEventListener(nomEvnt, handler, false);
            else {
                if (elt.detachEvent)
                    elt.detachEvent("on" + nomEvnt, handler);
                else
                    elt["on" + nomEvnt] = null;
            }
        },
 
        fixEvent: function (event) {
            var evt = event ? event : window.event;
            if (!evt.bubbles) evt.bubbles = evt.cancelBubble;
            if (!evt.cancelable) evt.cancelable = evt.returnValue;
            if (!evt.currentTarget) evt.currentTarget = evt.srcElement;
            if (!evt.preventDefault) evt.preventDefault = function () { evt.returnValue = false; };
            if (!evt.stopPropagation) evt.stopPropagation = function () { evt.cancelBubble = true; };
            if (!evt.target) evt.target = evt.srcElement;
            if (!evt.view) evt.view = window;
            if (!evt.relatedTarget) evt.relatedTarget = evt.fromElement ? evt.fromElement : evt.toElement;
            if (!evt.which) evt.which = evt.keyCode != 0 ? evt.keyCode: evt.charCode;
            if (!evt.key) evt.key = String.fromCharCode(evt.which);
            return evt;
        }
    };

DEMO: Anular menú contextual

En la siguiente página web, pulsando en el primer título con el botón derecho, se abrirá el menú contextual, pero no lo hará si lo hacemos en el segundo título (el que tiene oncontextmenu).

<!DOCTYPE html>
<html>
    <head>
        <title> </title>
    </head>
    <body>
        <h1>En esta línea SÍ se puede mostrar el menú contextual</h1>
        <h1 oncontextmenu="return false;">En esta linea sin embargo NO se mostrará</h1>
    </body>
</html>

DEMO: Detección de scroll

En la siguiente página, mientras hacemos scroll, la imagen de la izquierda se mantendrá en su posición.

<!DOCTYPE html>
<html>
    <head>
        <title> </title>
        <script type="text/javascript">
 
        // Manejador del evento 'onscroll' que detectará donde está la
        // imagen de portada y luego la reposiciona a partir de la posición
        // de scroll que ocupa en la página
        function moverPortada() {
            var portada = document.getElementById("portada");
            portada.style.top = document.documentElement.scrollTop + 10 + "px";
        }
 
        // añadimos una función al evento 'onscroll'
        document.onscroll = moverPortada;
 
        </script>
    </head>
    <body>
        <img id="portada" src="PortadaQuijote.jpg" style="position:absolute;left:10px;"/>
        <div style="margin-left:170px;">
            <h1>El ingenioso hidalgo Don Quixote de La Mancha</h1>
            <h2>Capítulo primero</h2>
            <h3>Que trata de la condición y ejercicio del famoso hidalgo D. Quijote de la Mancha</h3>
            <p>
            En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lentejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas con sus pantuflos de lo mismo, los días de entre semana se honraba con su vellori de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta, y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza, que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años, era de complexión recia, seco de carnes, enjuto de rostro; gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de Quijada o Quesada (que en esto hay alguna diferencia en los autores que deste caso escriben), aunque por conjeturas verosímiles se deja entender que se llama Quijana; pero esto importa poco a nuestro cuento; basta que en la narración dél no se salga un punto de la verdad. 
        </p>
        <p>
            Es, pues, de saber, que este sobredicho hidalgo, los ratos que estaba ocioso (que eran los más del año) se daba a leer libros de caballerías con tanta afición y gusto, que olvidó casi de todo punto el ejercicio de la caza, y aun la administración de su hacienda; y llegó a tanto su curiosidad y desatino en esto, que vendió muchas hanegas de tierra de sembradura, para comprar libros de caballerías en que leer; y así llevó a su casa todos cuantos pudo haber dellos; y de todos ningunos le parecían tan bien como los que compuso el famoso Feliciano de Silva: porque la claridad de su prosa, y aquellas intrincadas razones suyas, le parecían de perlas; y más cuando llegaba a leer aquellos requiebros y cartas de desafío, donde en muchas partes hallaba escrito: la razón de la sinrazón que a mi razón se hace, de tal manera mi razón enflaquece, que con razón me quejo de la vuestra fermosura, y también cuando leía: los altos cielos que de vuestra divinidad divinamente con las estrellas se fortifican, y os hacen merecedora del merecimiento que merece la vuestra grandeza. Con estas y semejantes razones perdía el pobre caballero el juicio, y desvelábase por entenderlas, y desentrañarles el sentido, que no se lo sacara, ni las entendiera el mismo Aristóteles, si resucitara para sólo ello. No estaba muy bien con las heridas que don Belianis daba y recibía, porque se imaginaba que por grandes maestros que le hubiesen curado, no dejaría de tener el rostro y todo el cuerpo lleno de cicatrices y señales; pero con todo alababa en su autor aquel acabar su libro con la promesa de aquella inacabable aventura, y muchas veces le vino deseo de tomar la pluma, y darle fin al pie de la letra como allí se promete; y sin duda alguna lo hiciera, y aun saliera con ello, si otros mayores y continuos pensamientos no se lo estorbaran.
        </p>
        <p>
            Tuvo muchas veces competencia con el cura de su lugar (que era hombre docto graduado en Sigüenza), sobre cuál había sido mejor caballero, Palmerín de Inglaterra o Amadís de Gaula; mas maese Nicolás, barbero del mismo pueblo, decía que ninguno llegaba al caballero del Febo, y que si alguno se le podía comparar, era don Galaor, hermano de Amadís de Gaula, porque tenía muy acomodada condición para todo; que no era caballero melindroso, ni tan llorón como su hermano, y que en lo de la valentía no le iba en zaga.
        </p>
        <p>
            En resolución, él se enfrascó tanto en su lectura, que se le pasaban las noches leyendo de claro en claro, y los días de turbio en turbio, y así, del poco dormir y del mucho leer, se le secó el cerebro, de manera que vino a perder el juicio. Llenósele la fantasía de todo aquello que leía en los libros, así de encantamientos, como de pendencias, batallas, desafíos, heridas, requiebros, amores, tormentas y disparates imposibles, y asentósele de tal modo en la imaginación que era verdad toda aquella máquina de aquellas soñadas invenciones que leía, que para él no había otra historia más cierta en el mundo.
        </p>
        <p>
            Decía él, que el Cid Ruy Díaz había sido muy buen caballero; pero que no tenía que ver con el caballero de la ardiente espada, que de sólo un revés había partido por medio dos fieros y descomunales gigantes. Mejor estaba con Bernardo del Carpio, porque en Roncesvalle había muerto a Roldán el encantado, valiéndose de la industria de Hércules, cuando ahogó a Anteo, el hijo de la Tierra, entre los brazos. Decía mucho bien del gigante Morgante, porque con ser de aquella generación gigantesca, que todos son soberbios y descomedidos, él solo era afable y bien criado; pero sobre todos estaba bien con Reinaldos de Montalbán, y más cuando le veía salir de su castillo y robar cuantos topaba, y cuando en Allende robó aquel ídolo de Mahoma, que era todo de oro, según dice su historia. Diera él, por dar una mano de coces al traidor de Galalón, al ama que tenía y aun a su sobrina de añadidura.
        </p>
        <p>
            En efecto, rematado ya su juicio, vino a dar en el más extraño pensamiento que jamás dio loco en el mundo, y fue que le pareció convenible y necesario, así para el aumento de su honra, como para el servicio de su república, hacerse caballero andante, e irse por todo el mundo con sus armas y caballo a buscar las aventuras, y a ejercitarse en todo aquello que él había leído, que los caballeros andantes se ejercitaban, deshaciendo todo género de agravio, y poniéndose en ocasiones y peligros, donde acabándolos, cobrase eterno nombre y fama.
        </p>
        <p>
            Imaginábase el pobre ya coronado por el valor de su brazo por lo menos del imperio de Trapisonda: y así con estos tan agradables pensamientos, llevado del estraño gusto que en ellos sentía, se dió priesa a poner en efecto lo que deseaba. Y lo primero que hizo, fue limpiar unas armas, que habían sido de sus bisabuelos, que, tomadas de orín y llenas de moho, luengos siglos había que estaban puestas y olvidadas en un rincón. Limpiólas y aderezólas lo mejor que pudo; pero vió que tenían una gran falta, y era que no tenía celada de encaje, sino morrión simple; mas a esto suplió su industria, porque de cartones hizo un modo de media celada, que encajada con el morrión, hacía una apariencia de celada entera. Es verdad que para probar si era fuerte, y podía estar al riesgo de una cuchillada, sacó su espada, y le dió dos golpes, y con el primero y en un punto deshizo lo que había hecho en una semana: y no dejó de parecerle mal la facilidad con que la había hecho pedazos, y por asegurarse de este peligro, lo tornó a hacer de nuevo, poniéndole unas barras de hierro por de dentro de tal manera, que él quedó satisfecho de su fortaleza; y, sin querer hacer nueva experiencia de ella, la diputó y tuvo por celada finísima de encaje. Fue luego a ver a su rocín, y aunque tenía más cuartos que un real, y más tachas que el caballo de Gonela, que tantum pellis, et ossa fuit, le pareció que ni el Bucéfalo de Alejandro, ni Babieca el del Cid con él se igualaban. Cuatro días se le pasaron en imaginar qué nombre le podría: porque, según se decía él a sí mismo, no era razón que caballo de caballero tan famoso, y tan bueno él por sí, estuviese sin nombre conocido; y así procuraba acomodársele, de manera que declarase quien había sido, antes que fuese de caballero andante, y lo que era entones: pues estaba muy puesto en razón, que mudando su señor estado, mudase él también el nombre; y le cobrase famoso y de estruendo, como convenía a la nueva orden y al nuevo ejercicio que ya profesaba: y así después de muchos nombres que formó, borró y quitó, añadió, deshizo y tornó a hacer en su memoria e imaginación, al fin le vino a llamar ROCINANTE, nombre a su parecer alto, sonoro y significativo de lo que había sido cuando fue rocín, antes de lo que ahora era, que era antes y primero de todos los rocines del mundo. Puesto nombre y tan a su gusto a su caballo, quiso ponérsele a sí mismo, y en este pensamiento, duró otros ocho días, y al cabo se vino a llamar DON QUIJOTE, de donde como queda dicho, tomaron ocasión los autores de esta tan verdadera historia, que sin duda se debía llamar Quijada, y no Quesada como otros quisieron decir. Pero acordándose que el valeroso Amadís, no sólo se había contentado con llamarse Amadís a secas, sino que añadió el nombre de su reino y patria, por hacerla famosa, y se llamó Amadís de Gaula, así quiso, como buen caballero, añadir al suyo el nombre de la suya, y llamarse DON QUIJOTE DE LA MANCHA, con que a su parecer declaraba muy al vivo su linaje y patria, y la honraba con tomar el sobrenombre della.
        </p>
        <p>
            Limpias, pues, sus armas, hecho del morrión celada, puesto nombre a su rocín, y confirmándose a sí mismo, se dió a entender que no le faltaba otra cosa, sino buscar una dama de quien enamorarse, porque el caballero andante sin amores, era árbol sin hojas y sin fruto, y cuerpo sin alma. Decíase él: si yo por malos de mis pecados, por por mi buena suerte, me encuentro por ahí con algún gigante, como de ordinario les acontece a los caballeros andantes, y le derribo de un encuentro, o le parto por mitad del cuerpo, o finalmente, le venzo y le rindo, ¿no será bien tener a quién enviarle presentado, y que entre y se hinque de rodillas ante mi dulce señora, y diga con voz humilde y rendida: yo señora, soy el gigante Caraculiambro, señor de la ínsula Malindrania, a quien venció en singular batalla el jamás como se debe alabado caballero D. Quijote de la Mancha, el cual me mandó que me presentase ante la vuestra merced, para que la vuestra grandeza disponga de mí a su talante? ¡Oh, cómo se holgó nuestro buen caballero, cuando hubo hecho este discurso, y más cuando halló a quién dar nombre de su dama! Y fue, a lo que se cree, que en un lugar cerca del suyo había una moza labradora de muy buen parecer, de quien él un tiempo anduvo enamorado, aunque según se entiende, ella jamás lo supo ni se dió cata de ello. Llamábase Aldonza Lorenzo, y a esta le pareció ser bien darle título de señora de sus pensamientos; y buscándole nombre que no desdijese mucho del suyo, y que tirase y se encaminase al de princesa y gran señora, vino a llamarla DULCINEA DEL TOBOSO, porque era natural del Toboso, nombre a su parecer músico y peregrino y significativo, como todos los demás que a él y a sus cosas había puesto.
        </p>
    </div>
</body>
</html>

Para navegadores antiguos, tendríamos que hacerlo de otra manera:

<!DOCTYPE html>
<html>
<head>
    <title> </title>
    <script type="text/javascript" src="EventHandlerHelper.js"></script>
    <script type="text/javascript">
 
        function moverPortada(event) {
         event = EventHandlerHelper.fixEvent(event);
         var portada = document.getElementById("portada");
         var scrollY = 0;
         // Si estamos en un navegador moderno:
         if (window.pageYOffset)
            scrollY = window.pageYOffset;
        else
            // Para navegadores antiguos (sobre todo Internet Explorer)
            scrollY = document.body.scrollTop;
        portada.style.top = scrollY + 10 + "px";
    }
 
    EventHandlerHelper.addEventListener(window, "scroll", moverPortada)
 
</script>
</head>
<body>
    <img id="portada" src="PortadaQuijote.jpg" style="position:absolute;left:10px;"/>
    <div style="margin-left:170px;">
        <h1>El ingenioso hidalgo Don Quixote de La Mancha</h1>
        <h2>Capítulo primero</h2>
        <h3>Que trata de la condición y ejercicio del famoso hidalgo D. Quijote de la Mancha</h3>
        <p>
            En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lentejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas con sus pantuflos de lo mismo, los días de entre semana se honraba con su vellori de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta, y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza, que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años, era de complexión recia, seco de carnes, enjuto de rostro; gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de Quijada o Quesada (que en esto hay alguna diferencia en los autores que deste caso escriben), aunque por conjeturas verosímiles se deja entender que se llama Quijana; pero esto importa poco a nuestro cuento; basta que en la narración dél no se salga un punto de la verdad. 
        </p>
        <p>
            Es, pues, de saber, que este sobredicho hidalgo, los ratos que estaba ocioso (que eran los más del año) se daba a leer libros de caballerías con tanta afición y gusto, que olvidó casi de todo punto el ejercicio de la caza, y aun la administración de su hacienda; y llegó a tanto su curiosidad y desatino en esto, que vendió muchas hanegas de tierra de sembradura, para comprar libros de caballerías en que leer; y así llevó a su casa todos cuantos pudo haber dellos; y de todos ningunos le parecían tan bien como los que compuso el famoso Feliciano de Silva: porque la claridad de su prosa, y aquellas intrincadas razones suyas, le parecían de perlas; y más cuando llegaba a leer aquellos requiebros y cartas de desafío, donde en muchas partes hallaba escrito: la razón de la sinrazón que a mi razón se hace, de tal manera mi razón enflaquece, que con razón me quejo de la vuestra fermosura, y también cuando leía: los altos cielos que de vuestra divinidad divinamente con las estrellas se fortifican, y os hacen merecedora del merecimiento que merece la vuestra grandeza. Con estas y semejantes razones perdía el pobre caballero el juicio, y desvelábase por entenderlas, y desentrañarles el sentido, que no se lo sacara, ni las entendiera el mismo Aristóteles, si resucitara para sólo ello. No estaba muy bien con las heridas que don Belianis daba y recibía, porque se imaginaba que por grandes maestros que le hubiesen curado, no dejaría de tener el rostro y todo el cuerpo lleno de cicatrices y señales; pero con todo alababa en su autor aquel acabar su libro con la promesa de aquella inacabable aventura, y muchas veces le vino deseo de tomar la pluma, y darle fin al pie de la letra como allí se promete; y sin duda alguna lo hiciera, y aun saliera con ello, si otros mayores y continuos pensamientos no se lo estorbaran.
        </p>
        <p>
            Tuvo muchas veces competencia con el cura de su lugar (que era hombre docto graduado en Sigüenza), sobre cuál había sido mejor caballero, Palmerín de Inglaterra o Amadís de Gaula; mas maese Nicolás, barbero del mismo pueblo, decía que ninguno llegaba al caballero del Febo, y que si alguno se le podía comparar, era don Galaor, hermano de Amadís de Gaula, porque tenía muy acomodada condición para todo; que no era caballero melindroso, ni tan llorón como su hermano, y que en lo de la valentía no le iba en zaga.
        </p>
        <p>
            En resolución, él se enfrascó tanto en su lectura, que se le pasaban las noches leyendo de claro en claro, y los días de turbio en turbio, y así, del poco dormir y del mucho leer, se le secó el cerebro, de manera que vino a perder el juicio. Llenósele la fantasía de todo aquello que leía en los libros, así de encantamientos, como de pendencias, batallas, desafíos, heridas, requiebros, amores, tormentas y disparates imposibles, y asentósele de tal modo en la imaginación que era verdad toda aquella máquina de aquellas soñadas invenciones que leía, que para él no había otra historia más cierta en el mundo.
        </p>
        <p>
            Decía él, que el Cid Ruy Díaz había sido muy buen caballero; pero que no tenía que ver con el caballero de la ardiente espada, que de sólo un revés había partido por medio dos fieros y descomunales gigantes. Mejor estaba con Bernardo del Carpio, porque en Roncesvalle había muerto a Roldán el encantado, valiéndose de la industria de Hércules, cuando ahogó a Anteo, el hijo de la Tierra, entre los brazos. Decía mucho bien del gigante Morgante, porque con ser de aquella generación gigantesca, que todos son soberbios y descomedidos, él solo era afable y bien criado; pero sobre todos estaba bien con Reinaldos de Montalbán, y más cuando le veía salir de su castillo y robar cuantos topaba, y cuando en Allende robó aquel ídolo de Mahoma, que era todo de oro, según dice su historia. Diera él, por dar una mano de coces al traidor de Galalón, al ama que tenía y aun a su sobrina de añadidura.
        </p>
        <p>
            En efecto, rematado ya su juicio, vino a dar en el más extraño pensamiento que jamás dio loco en el mundo, y fue que le pareció convenible y necesario, así para el aumento de su honra, como para el servicio de su república, hacerse caballero andante, e irse por todo el mundo con sus armas y caballo a buscar las aventuras, y a ejercitarse en todo aquello que él había leído, que los caballeros andantes se ejercitaban, deshaciendo todo género de agravio, y poniéndose en ocasiones y peligros, donde acabándolos, cobrase eterno nombre y fama.
        </p>
        <p>
            Imaginábase el pobre ya coronado por el valor de su brazo por lo menos del imperio de Trapisonda: y así con estos tan agradables pensamientos, llevado del estraño gusto que en ellos sentía, se dió priesa a poner en efecto lo que deseaba. Y lo primero que hizo, fue limpiar unas armas, que habían sido de sus bisabuelos, que, tomadas de orín y llenas de moho, luengos siglos había que estaban puestas y olvidadas en un rincón. Limpiólas y aderezólas lo mejor que pudo; pero vió que tenían una gran falta, y era que no tenía celada de encaje, sino morrión simple; mas a esto suplió su industria, porque de cartones hizo un modo de media celada, que encajada con el morrión, hacía una apariencia de celada entera. Es verdad que para probar si era fuerte, y podía estar al riesgo de una cuchillada, sacó su espada, y le dió dos golpes, y con el primero y en un punto deshizo lo que había hecho en una semana: y no dejó de parecerle mal la facilidad con que la había hecho pedazos, y por asegurarse de este peligro, lo tornó a hacer de nuevo, poniéndole unas barras de hierro por de dentro de tal manera, que él quedó satisfecho de su fortaleza; y, sin querer hacer nueva experiencia de ella, la diputó y tuvo por celada finísima de encaje. Fue luego a ver a su rocín, y aunque tenía más cuartos que un real, y más tachas que el caballo de Gonela, que tantum pellis, et ossa fuit, le pareció que ni el Bucéfalo de Alejandro, ni Babieca el del Cid con él se igualaban. Cuatro días se le pasaron en imaginar qué nombre le podría: porque, según se decía él a sí mismo, no era razón que caballo de caballero tan famoso, y tan bueno él por sí, estuviese sin nombre conocido; y así procuraba acomodársele, de manera que declarase quien había sido, antes que fuese de caballero andante, y lo que era entones: pues estaba muy puesto en razón, que mudando su señor estado, mudase él también el nombre; y le cobrase famoso y de estruendo, como convenía a la nueva orden y al nuevo ejercicio que ya profesaba: y así después de muchos nombres que formó, borró y quitó, añadió, deshizo y tornó a hacer en su memoria e imaginación, al fin le vino a llamar ROCINANTE, nombre a su parecer alto, sonoro y significativo de lo que había sido cuando fue rocín, antes de lo que ahora era, que era antes y primero de todos los rocines del mundo. Puesto nombre y tan a su gusto a su caballo, quiso ponérsele a sí mismo, y en este pensamiento, duró otros ocho días, y al cabo se vino a llamar DON QUIJOTE, de donde como queda dicho, tomaron ocasión los autores de esta tan verdadera historia, que sin duda se debía llamar Quijada, y no Quesada como otros quisieron decir. Pero acordándose que el valeroso Amadís, no sólo se había contentado con llamarse Amadís a secas, sino que añadió el nombre de su reino y patria, por hacerla famosa, y se llamó Amadís de Gaula, así quiso, como buen caballero, añadir al suyo el nombre de la suya, y llamarse DON QUIJOTE DE LA MANCHA, con que a su parecer declaraba muy al vivo su linaje y patria, y la honraba con tomar el sobrenombre della.
        </p>
        <p>
            Limpias, pues, sus armas, hecho del morrión celada, puesto nombre a su rocín, y confirmándose a sí mismo, se dió a entender que no le faltaba otra cosa, sino buscar una dama de quien enamorarse, porque el caballero andante sin amores, era árbol sin hojas y sin fruto, y cuerpo sin alma. Decíase él: si yo por malos de mis pecados, por por mi buena suerte, me encuentro por ahí con algún gigante, como de ordinario les acontece a los caballeros andantes, y le derribo de un encuentro, o le parto por mitad del cuerpo, o finalmente, le venzo y le rindo, ¿no será bien tener a quién enviarle presentado, y que entre y se hinque de rodillas ante mi dulce señora, y diga con voz humilde y rendida: yo señora, soy el gigante Caraculiambro, señor de la ínsula Malindrania, a quien venció en singular batalla el jamás como se debe alabado caballero D. Quijote de la Mancha, el cual me mandó que me presentase ante la vuestra merced, para que la vuestra grandeza disponga de mí a su talante? ¡Oh, cómo se holgó nuestro buen caballero, cuando hubo hecho este discurso, y más cuando halló a quién dar nombre de su dama! Y fue, a lo que se cree, que en un lugar cerca del suyo había una moza labradora de muy buen parecer, de quien él un tiempo anduvo enamorado, aunque según se entiende, ella jamás lo supo ni se dió cata de ello. Llamábase Aldonza Lorenzo, y a esta le pareció ser bien darle título de señora de sus pensamientos; y buscándole nombre que no desdijese mucho del suyo, y que tirase y se encaminase al de princesa y gran señora, vino a llamarla DULCINEA DEL TOBOSO, porque era natural del Toboso, nombre a su parecer músico y peregrino y significativo, como todos los demás que a él y a sus cosas había puesto.
        </p>
    </div>
</body>
</html>

El contenido de EventHandlerHelper.js está en una sección anterior.

DEMO: Desactivar un botón de radio

En este apartado tratamos de solucionar un problema bastante común con los botones 'radio': una vez que seleccionamos una opción, no podemos pulsar de nuevo para “deseleccionarla”.

Partimos de la siguiente página:

<!DOCTYPE html> <html> <head>

  <title> </title>

</head> <body>

  <h2>Primer grupo de opciones:</h2>
  <input type="radio" name="grp1" id="g1op1" /><label for="g1op1">Opción 1</label><br />
  <input type="radio" name="grp1" id="g1op2" /><label for="g1op2">Opción 2</label><br />
  <input type="radio" name="grp1" id="g1op3" /><label for="g1op3">Opción 3</label>
  <h2>Segundo grupo de opciones:</h2>
  <input type="radio" name="grp2" id="g2op1" /><label for="g2op1">Opción A</label><br />
  <input type="radio" name="grp2" id="g2op2" /><label for="g2op2">Opción B</label><br />
  <input type="radio" name="grp2" id="g2op3" /><label for="g2op3">Opción C</label>

</body> </html>

Si probamos, al marcar una opción, no podremos desmarcarla. Siempre quedará marcada una hasta que volvamos a abrir la página.

Posible solución:

<!DOCTYPE html>
<html>
    <head>
        <title> </title>
        <script type="text/javascript" src="EventHandlerHelper.js"></script>
        <script type="text/javascript">
 
        // Objeto global vacío para guardar la última selección de 
        // cada grupo de botones de radio
        var currSel = {};
 
        //Deselecciona el botón de radio seleccionado si se vuelve a seleccionar
        function optDeselect(event)
        {
            event = EventHandlerHelper.fixEvent(event);
            // Qué botón está generando el evento?
            var opt = event.target;
 
            // Comprobamos si el id del botón de radio actualmente pulsado es
            // el que está guardado 
            if (opt.id == currSel[opt.name]) {
                // Lo 'deseleccionamos':
                opt.checked = false;
                currSel[opt.name] = "";
            }
            else {
                currSel[opt.name] = opt.id;
            }
        }
 
        function initialize()
        {
            // busco todos los botones de radio y les asigno el manejador 
            // 'optDeselect' para sus eventos 'click'
            var radios = document.getElementsByTagName("input");
            for (var i = 0; i < radios.length; i++)
            {
                if (radios[i].type == "radio")
                    // Asignamos el evento 'click' al manejador
                    EventHandlerHelper.addEventListener(radios[i], "click", optDeselect);
            }
        }
 
        window.onload = initialize;
        </script>
    </head>
    <body>
        <h2>Primer grupo de opciones:</h2>
        <input type="radio" name="grp1" id="g1op1" /><label for="g1op1">Opción 1</label><br />
        <input type="radio" name="grp1" id="g1op2" /><label for="g1op2">Opción 2</label><br />
        <input type="radio" name="grp1" id="g1op3" /><label for="g1op3">Opción 3</label>
 
        <h2>Segundo grupo de opciones:</h2>
        <input type="radio" name="grp2" id="g2op1" /><label for="g2op1">Opción A</label><br />
        <input type="radio" name="grp2" id="g2op2" /><label for="g2op2">Opción B</label><br />
        <input type="radio" name="grp2" id="g2op3" /><label for="g2op3">Opción C</label>
    </body>
</html>

Eventos del teclado

Otra gran necesidad que tienen los programas es la de interactuar con los usuarios a través del teclado del ordenador. Cuando un usuario rellena un campo en un formulario, podemos determinar qué teclas está pulsando y permitirle únicamente que utilice números, por ejemplo.

Existen cuatro eventos principales relacionados con el teclado que nos puede notificar el navegador:

  • keydown: se genera en el instante en el que el usuario pulsa una tecla y mientras la mantiene pulsada. Se notifica para el elemento que tenga el foco en ese momento (generalmente cajas de texto, pero puede ser cualquier elemento en realidad). Este evento también salta cuando se pulsan teclas de control o caracteres no imprimibles.
  • keypress: es igual al anterior pero solamente para caracteres imprimibles. Se genera continuamente mientras no se suelte la tecla, una vez por cada carácter que se va a mostrar en una caja de texto. Algunas excepciones son la pulsación de las teclas ESC y ENTER que aunque no son visibles se notifican también. Por ejemplo, con este evento no detectaríamos la pulsación de las flechas del cursor, mientras que con keydown sí. En la actualidad se considera un evento obsoleto, pero lo cierto es que se sigue utilizando mucho y no va a desaparecer de los navegadores.
  • input: se notifica cuando el texto que contiene un control editable (como una caja de texto) ha cambiado. También existe un evento beforeInput que salta justo antes de que cambie el contenido, pero tiene una utilidad limitada y lo verás usado en pocas ocasiones.
  • keyup: se notifica cuando el usuario suelta la tecla que tenía presionada.

El orden en el que saltan los eventos es el mismo en el que aparecen en la lista anterior. Si el usuario deja una tecla pulsada saltarán los eventos keydown, keypress e input repetidamente hasta que la suelte, momento en el que se notificará el keyup. Si lo que se pulsa es un carácter no imprimible o una tecla de control el evento keypress no saltará.

Cuando se publicó la versión 2 del DOM y apareció el primer modelo de eventos, los eventos relacionados con el teclado quedaron fuera, y se siguió usando el modelo antiguo heredado del BOM. Los tres eventos keyX de la lista son, por tanto, tan antiguos como los primeros navegadores y han funcionado igual desde siempre, por lo que son un estándar de facto aunque no esté refrendado por el W3C. Más adelante, cuando se presentó el DOM Level 3, le añadieron eventos del teclado estandarizados, y más específicamente un nuevo evento denominado textInput. Este evento sólo lo implementaron Chrome, Safari y las versiones de Internet Explorer superiores a la 9. Más adelante decidieron eliminarlo y dejar tan solo los eventos input y beforeInput, que hemos visto más arriba.

Una vez que se notifica cualquiera de estos eventos obtendremos información detallada sobre la acción del usuario a través del objeto event y sus propiedades. Existen cuatro propiedades muy parecidas en el evento que nos proporcionan información sobre la tecla pulsada: keyCode, charCode, which, y key. Las tres primeras devuelven el código Unicode de la tecla pulsada, pero son dependientes del sistema operativo y navegador por lo que podrían cambiar, mientras que key devuelve siempre una cadena de texto que representa la tecla pulsada, incluyendo valores no imprimibles. Por ejemplo, si se pulsa la tecla “a” al mismo tiempo que se pulsa mayúsculas, la tecla resultante es “A”, que será lo que devuelva la propiedad key. De este modo no tendremos que comprobar si se está pulsando o no la tecla de mayúsculas.

os tres primeros se consideran obsoletos y en navegadores modernos deberíamos utilizar key siempre.

Todavía quedan un par de particularidades adicionales que debemos tener en cuenta (y que pueden cambiar con el tiempo, puesto que estas cosas no son estáticas, asi que ojo):

  • No se notifica keypress al pulsar la tecla ESC ni otros caracteres imprimibles, a excepción de ENTER, CTRL + ENTER y SHIFT + ENTER. De todos modos, debes comprobarlo siempre si lo necesitas, puesto que puede variar de un navegador a otro o entre versiones.
  • En el evento keydown, independientemente de si la tecla se pulsa con mayúsculas o minúsculas activado, se devuelve el identificador Unicode de la tecla mayúscula. Sin embargo en keypress se devuelve el código de la letra que va a visualizarse. Así, por ejemplo si pulsamos la letra “a” minúscula, en keydown se notificará el número 65 (código ASCII de la “a” mayúscula en realidad), mientras que en keypress sería el 97 (el verdadero para esa letra). Esto tiene lógica porque se supone que con keydown estamos tratando de identificar la tecla que se pulsa mientras que en keypress el carácter que se va a introducir.

OJO: esto tiene como implicación que deberíamos separar adecuadamente entre los dos eventos el control de las teclas, dejando el keydown para teclas de control (por ejemplo, detectar que se ha pulsado ENTER) y el keypress (o mejor input) para cuando necesitemos controlar la entrada de datos del usuario. Si es necesario utilizaremos los dos, ocupándose cada uno de la parte que le corresponde.

Estos códigos de tecla se pueden combinar con las propiedades Shiftkey, ctrlKey, altKey y metaKey, ya vistas para los eventos de ratón, para saber si además estaban pulsadas esas teclas especiales durante el evento (que también se detectarán en keydown).

La siguiente tabla muestra los códigos de las teclas más útiles que podemos detectar en keydown y keyup:

Tecla Código Tecla Código
Borrado 8 Tabulador 9
ENTER 13 Mayúsculas 16
Control 17 Alt 18
Pause 19 Bloqueo Mays 20
ESC 27 Página Arriba 33
Página abajo 34 Fin 35
Inicio 36 37
38 39
40 Insertar 45
Suprimir 46 Windows Izq. 91
Windows Der. 92 Menú Context. 93
0-9 (tecl. num.) 96-105 * (tecl. numérico) 106
+ (tec. num.) 107 - 109
. (tec. num.) 110 / (tec. num.) 111
F1-F12 112-123 Bloq. Números 144
Bloqueo Despl. 145

Existe una propiedad adicional llamada repeat que contiene un valor booleano para indicar si el usuario ha dejado pulsada o no la tecla que estamos detectando (pero no funciona ni en IE ni en versiones pre-Chromium de Edge).

DEMO: Permitir solo valores numéricos

En este ejemplo uso la propiedad which porque estoy empleando nuestro manejador universal, pero en un ejemplo real deberías utilizar la propiedad key. De hecho, intenta recrear este ejemplo y el siguiente con la propiedad key del evento y sin EventHandlerHelper.js y te servirá como un buen ejercicio para practicar.

La siguiente página tiene dos cuadros de texto donde solo se podrán introducir números, hasta un máximo de 5 cifras en uno, y 4 en el segundo.

También veremos cómo controlar el pegado mediante Ctrl + V o menú contextual.

<!DOCTYPE html>
<html>
<head>
    <title> </title>
    <script type="text/javascript" src="EventHandlerHelper.js"></script>
    <script type="text/javascript">
 
        // Para el evento keydown porque verifica si estamos introduciendo
        // un número desde el teclado numérico. Además el 8 es la tecla de suprimir,
        // y del 35 al 46 están los cursores, borrado hacia atrás, inicio, fin...
        function esTeclaNumeroOCursor(n) {
            return (n >= 48 && n <=57) || (n >= 96 && n <=105) || (n == 8) || (n >= 35 && n <=46 )
        }
 
        // Para el evento keypress
        function esCaracterNumero(n) {
            // Del 48 al 57 son los valores ASCII de los números del 0 al 9
            return (n >= 48 && n <= 57)
        }
 
        // Manejador de evento que verifica que solo sean números y 
        // de una longitud máxima
        function soloNumeros(max) {
            return function (event) {
                event = EventHandlerHelper.fixEvent(event);
                var input = event.target;
                if (!esCaracterNumero(event.which) || input.value.length >= max)
                    // Evitamos el comportamiento por defecto del evento que,
                    // en este caso, es la pulsación de la tecla
                    event.preventDefault();
            };
        }
 
        //Evita que se pegue usando CTRL+V
        function evitaPegado(event) {
            event = EventHandlerHelper.fixEvent(event);
            if ( event.which == 86 && event.ctrlKey == true)
                event.preventDefault();
        }
 
        function cancelaEvento(event) {
            event.preventDefault();
        }
 
        function initialize() {
            //Solo números y longitud máxima
            EventHandlerHelper.addEventListener(document.getElementById("datos"), "keypress", soloNumeros(5));
            EventHandlerHelper.addEventListener(document.getElementById("datos2"), "keypress", soloNumeros(4));
 
            //Prohibir pegar del portapapeles con CTRL + V
            //No sirve de mucho por el menú contextual (descomentar para verlo)
            //EventHandlerHelper.addEventListener(document.getElementById("datos"), "keydown", evitaPegado);
            //EventHandlerHelper.addEventListener(document.getElementById("datos2"), "keydown", evitaPegado);
 
            //Evitar menú contextual (NO SIRVE en IE o Firefox, descomentar para verlo)
            //EventHandlerHelper.addEventListener(document.getElementById("datos"), "contextmenu", cancelaEvento);
            //EventHandlerHelper.addEventListener(document.getElementById("datos2"), "contextmenu", cancelaEvento);
 
            //Evitar el pegado (no sirve en Opera)
            EventHandlerHelper.addEventListener(document.getElementById("datos"), "paste", cancelaEvento);
            EventHandlerHelper.addEventListener(document.getElementById("datos2"), "paste", cancelaEvento);
        }
 
        window.onload = initialize;
    </script>
</head>
<body>
    <label for="datos">Sólo permite introducir números: </label><input id="datos" size="10" />
    <br />
    <label for="datos2">Otro que sólo permite introducir números: </label><input id="datos2" size="10" />
</body>
</html>

DEMO: Control básico de un marcianito

En la siguiente página HTML veremos cómo usando los eventos de teclado, en este caso los cursores y la tecla Ctrl, podremos mover una imagen por la pantalla.

<!DOCTYPE html>
<html>
    <head>
        <title> </title>
        <script type="text/javascript" src="EventHandlerHelper.js"></script>
        <script type="text/javascript">
 
        //Constante para decidir el incremento de movimiento por cada pulsación de flecha
        var INCREMENTO = 3;
        //Multiplica la velocidad por esta constante en modo turbo
        var TURBO_EXTRA = 20;
        var marciano;   //para apuntar al marcianito
 
        //Manejador para el control de las pulsaciones de las flechas / cursores
        function controlCursor(event) {
            event = EventHandlerHelper.fixEvent(event);
            var deltaX = 0, deltaY = 0;
            switch (event.which)
            {
                case 37:    //Flecha izquierda
                    deltaX = -GetIncremento(event.ctrlKey);
                    break;
                case 38:    //Flecha arriba
                    deltaY = -GetIncremento(event.ctrlKey);
                    break;
                case 39:    //Flecha derecha
                    deltaX = GetIncremento(event.ctrlKey);
                    break;
                case 40:    //Flecha abajo
                    deltaY = GetIncremento(event.ctrlKey);
                    break;
            }
            //Incrementamos en "delta" la posición horizontal y vertical del marcianito
            moverMarciano(deltaX, deltaY);
        }
 
        function GetIncremento(turboOn) {
            // Si el turbo está "on" (tecla Ctrl pulsada), incrementamos 
            // el valor del incremento
            if (turboOn)
                return INCREMENTO + TURBO_EXTRA;
            else
                return INCREMENTO;
        }
 
        //Se encarga de mover el marcianito
        function moverMarciano(deltaX, deltaY) {
            var posX = parseInt(marciano.style.left) + deltaX;
            var posY = parseInt(marciano.style.top) + deltaY;
            // Controlamos los límites (evitar que se salga por los bordes
            // de la página)
            if (posX <= 0) posX = 0;
            if (posX >= document.documentElement.clientWidth) posX = document.documentElement.clientWidth - marciano.width;
            if (posY <= 0) posY = 0;
            if (posY >= document.documentElement.clientHeight) posY = document.documentElement.clientHeight - marcianito.height;
            marciano.style.left = posX + "px";
            marciano.style.top = posY + "px";
        }
 
        function initialize() {
            //averiguamos cuál es el centro de la pantalla
            var ancho = document.documentElement.clientWidth;
            var alto = document.documentElement.clientHeight;
            //Colocamos el marcinanito en el centro
            marciano = document.getElementById("marcianito");
            marciano.style.left = ((ancho - marciano.width) / 2) + "px";
            marciano.style.top = ((alto - marciano.height) / 2) + "px";
            //controlamos su movimiento con las teclas
            EventHandlerHelper.addEventListener(document.documentElement, "keydown", controlCursor);
        }
 
        window.onload = initialize;
        </script>
    </head>
    <body>
        <span style="font-size:xx-small;">Pulsa las flechas para mover al marciano.<br />
        Pulsa CTRL para acelerar el movimiento.</span>
 
        <img id="marcianito" src="invader.png" style="position:absolute;" />
    </body>
</html>

El anterior código JavaScript no permite detectar dos teclas a la vez, por si queremos hacer desplazamientos suaves en diagonal. Esto no se puede resolver solo con eventos, son necesarias técnicas más avanzadas.

Otros eventos

Existen muchos eventos que pueden producirse en una página, aunque ya hemos visto la mayoría de los interesantes para el día a día. Muchos son exclusivos de ciertos navegadores o de ciertos dispositivos, como por ejemplo los eventos relacionados con gestos multi-touch o con cambios de orientación de pantalla, exclusivos para móviles y tabletas (no los estudiaremos en este curso, pero son muy sencillos y puedes verlos en los enlaces anteriores). Otros tienen aplicaciones muy especializadas que raramente usarás.

En la MDN tienes una referencia sensacional de todos ellos y te recomiendo que visites la página cuando tengas alguna duda o quieras conocer el soporte que ofrecen los navegadores de cada uno de ellos.

En este último apartado vamos a señalar algunos de los más interesantes que debes tener en cuenta.

Eventos de formularios y controles de entrada

formulario

Para el caso del formulario ya hemos visto el más importante de ellos, submit, que salta cuando se intenta enviar el formulario al servidor (y que ya hemos visto en algún vídeo práctico). También dispone de otro relacionado llamado reset que se notifica cuando se inicializa el formulario pulsando sobre un botón de tipo reset.

Los controles contenidos en el formulario tiene sus propios eventos también, aparte de los que ya hemos visto relacionados con el ratón y el teclado:

  • focus: se notifica cuando el elemento coge el foco del teclado, bien porque pulsamos encima o porque nos movemos sobre él pulsando el tabulador.
  • blur: el contrario al anterior. Se lanza cuando el elemento pierde el foco.
  • change: se notifica en una caja de texto (tanto <input type="text"> o en <textarea>) cuando pierde el foco y el contenido ha cambiado respecto al momento en que cogió el foco.
  • select: este evento se genera cuando el usuario selecciona con el cursor un texto contenido en un elemento. Funciona en los controles de texto de los formularios para todos los navegadores. En otros elementos suele funcionar pero depende del navegador y de su versión. Se notifica incluso cuando la selección se realiza mediante código, usando el método select de los controles de formulario (selecciona el contenido completo del control).

Eventos de mutación del DOM

Otro conjunto de eventos que pueden tener interés en aplicaciones específicas son aquellos que se refieren a las modificaciones del DOM. Como hemos visto cuando lo estudiamos, es muy sencillo modificar la estructura jerárquica del DOM y agregar y quitar nodos, moverlos, etc… Existen una serie de eventos estándar que nos permiten obtener notificaciones cuando hay cualquier cambio en la jerarquía del documento:

  • DOMNodeInserted: se notifica cuando se ha insertado un elemento como hijo de otro. Se obtiene una referencia al elemento en cuestión consultando la propiedad data del objeto event desde dentro del manejador.
  • DOMNodeRemoved: cuando se elimina un nodo.
  • DOMAttrModified: cuando se modifica un atributo de un elemento. No está soportado por Chrome ni Safari.
  • DOMCharacterDataModified: se genera cuando se cambia mediante código el contenido de un nodo de texto. El nuevo valor se obtiene de la propiedad data del elemento cambiado en cuestión: event.target.data.
  • DOMSubtreeModified: salta ante cualquier cambio de la estructura del DOM de la página. Podemos centralizar la detección de todos los cambios en él pues salta siempre, justo después de los anteriores. No está soportado por Opera.

Y por cierto, sí, los nombres de estos eventos son mezclando mayúsculas y minúsculas, al contrario de los habituales, por lo que hay que añadirlos exactamente así en la función addEventListener.

Se usan raramente y solo para aplicaciones especializadas. Además han pasado a considerarse obsoletos ya que han sido sustituidos por los Mutation Observers. En cualquier caso, es una técnica especializada con aplicaciones muy específicas que no utilizaremos en el día a día del desarrollo web, por lo que no comentaremos nada más aquí.

Otros eventos sueltos

Todavía quedan algunos eventos más que son comunes a (casi) todos los navegadores y que me gustaría al menos mencionar:

  • resize: salta cuando se cambia el tamaño de un elemento.
  • error: se produce cuando hay algún error de script en la página. Lo vemos con detalle en el módulo dedicado a la depuración de programas JavaScript.
  • message: se utiliza para recibir mensajes enviados por otros marcos del mismo dominio utilizando el método postMessage. Se utiliza para comunicación sencilla de datos entre scripts pertenecientes a páginas diferentes dentro del mismo dominio. Forma parte de la API específica de mensajes web de HTML5 y se sale del ámbito de este curso, pero conviene saber que existe.

Ejemplo: una biblioteca avanzada para la validación de formularios

Como ejemplo que nos permita ver algunos eventos de formularios, pero sobre todo practicar con código lo más real posible, he creado una biblioteca especializada que nos servirá para validar cualquier formulario de una página web de manera simple. Se trata de campusMVP_Validador.js. La puedes descargar desde el lateral o desde la zona de descargas del curso.

En esta biblioteca podrás ver código que hace uso de diversas técnicas JavaScript avanzadas, como por ejemplo entre otras cosas:

  • Uso de módulos para evitar conflictos de nomenclaturas y poder usarla en cualquier página con tranquilidad
  • Sacar partido a las clausuras
  • Definición y uso de atributos HTML propios para controlar el comportamiento de los elementos
  • Cambio de contexto de las llamadas (control del valor de this)
  • Código modular, ordenado y fácil de mantener/ampliar

Esta biblioteca es de código abierto y si lo deseas la puedes utilizar en cualquier proyecto propio mientras no retires ninguna de las notas informativas de crédito (licencia Creative Commons CC BY).

La he orientado a su uso en el mercado de habla hispana, por lo que todos los atributos de validación están en español.

La biblioteca funciona con cualquier navegador, incluso los más antiguos, y no depende de ninguna otra biblioteca como jQuery o similar. Es JavaScript puro y autocontenido.

El código se entrega completo con comentarios detallados para seguir su implementación, y también en versión minimizada para que ocupe lo menos posible y se use eficientemente. La versión minimizada ocupa menos de 3KB.

NOTA IMPORTANTE: en el ZIP existen dos versiones de la biblioteca. La de la carpeta campusMVP.validador-sin-data- es la que se muestra en los vídeos y se documenta a continuación. La versión en campusMVP.validator es una versión específica para que se valide sin problemas con cualquier validador de sintaxis de HTML5. Esta versión utiliza atributos "data-" propios de HTML5, en lugar de atributos totalmente libres. El motivo es que estos son los únicos atributos propios válidos en el estándar HTML5. Si necesitas que tu proyecto valide la sintaxis de HTML5, deberás usar esta versión y es la que te recomiendo. Si la usas todo lo mostrado en este documento y en los vídeos, es exactamente igual solo que lleva el prefijo data- delante. Por ejemplo, si queremos que un campo sea obligatorio pondríamos data-val-obligatorio en lugar de solamente val-obligatorio. Por lo demás se usan y funcionan exactamente igual.

Cómo funciona la biblioteca de validación

La parte de “usuario” programador de esta biblioteca es realmente sencilla de utilizar.

Lo único que tienes que hacer es incluirla en tus páginas:

<script type="text/javascript" src="campusMVP_Validador.min.js"></script>

A partir de ese instante podrás validar de manera automática o semiautomática (dependiendo de tus necesidades) cualquier formulario simplemente decorando los campos con ciertos atributos.

Todos los atributos comienzan con el prefijo val- (de validación) para distinguirlos de los demás atributos posibles.

Por defecto, estos son los atributos disponibles, los cuales se pueden combinar entre sí aplicando varios a un mismo campo del formulario:

  • val-obligatorio: si se decora un campo con este atributo se convierte en obligatorio y el usuario debe introducir algo para darlo por válido.
  • val-longmin: define la longitud mínima que debe tener el campo en caso de introducirse algún valor. Si no es obligatorio no saltaría esta validación si se deja en blanco.
  • val-longmax: define la longitud máxima que debe puede tener el campo en caso de introducirse algún valor.
  • val-num: fuerza que el valor introducido deba ser un número para poder darlo por válido. Admite tanto números enteros como decimales, usando el separador de miles establecido para la cultura actual (por ejemplo, la coma en español o el punto en inglés).
  • val-entero: fuerza a que el valor introducido deba ser un número entero para poder darlo por válido. Si se introduce algo que no sea un número entero válido (positivo o negativo) el campo no pasará la validación.
  • val-date: fuerza a que el valor introducido deba ser una fecha para poder darlo por válido. La fecha debe tener obligatoriamente el formato “dd/mm/yyyy”, aunque admite solo un número para el día o el mes, y solo 2 para el caso del año (por ejemplo: 2/7/2020).
  • val-rango: permite especificar un rango de números entre los cuales debe estar comprendido el valor que se introduzca. Normalmente irá combinado con el atributo val-num o val-entero. Permite especificar rangos de números positivos o negativos, con o sin decimales. Los extremos del rango se deben separar con un guion. Por ejemplo: val-rango="1-50", permitirá únicamente números entre el 1 y el 50.
  • val-rangofechas: permite especificar un rango de fechas entre las cuales debe estar comprendida la fecha introducida en el campo. Se suele combinar con val-fecha. Permite especificar solo el extremo inferior del intervalo. Por ejemplo: val-rangoFechas="1/1/1900-31/12/2015" permitiría introducir únicamente fechas comprendidas entre el 1 de enero de 1900 y el 31 de diciembre de 2015, y val-rangoFechas="1/1/1900" permitiría introducir cualquier fecha posterior al 1 de enero de 1900.
  • val-email: si se especifica, fuerza a que el texto introducido en un campo sea una dirección de correo electrónico válida (en el sentido de bien formada, no comprueba que exista, obviamente). Para la validación usa una expresión regular.
  • val-custom: permite definir nuestra propia función de validación. De este modo podemos llevar a cabo validaciones complejas, por ejemplo un campo que deba ser obligatorio o no en función del valor de otro, o cualquier otra cosa que se nos ocurra y que no esté contemplada con las validaciones por defecto. Dentro del contexto de la función que hayamos definido, cuando la llame el código de validación la palabra clave this apuntará al campo que se está validando en ese momento, lo cual redunda en una gran comodidad para el programador. Se le pueden pasar los parámetros que deseemos. La función debe cumplir solamente dos condiciones sencillas:
    • Debe estar definida a nivel global, es decir, accesible desde cualquier parte del código de la página. Si no existiera, el código de validación fallaría y devolvería automáticamente un false.
    • Debe devolver true si el campo pasa la validación o false en caso contrario.
  • val-mensaje: Es el mensaje que queremos mostrar al usuario en caso de que la validación de un campo no se supere.

Los atributos no dependen de mayúsculas y minúsculas. Es decir, podemos escribir un atributo como “val-longmin”, “Val-LongMin” o “VAL-LONGMIN”, como queramos, y funcionará en cualquier caso. De todos modos, por coherencia con HTML, mi recomendación sería usarlos en minúsculas, aunque por claridad también es interesante usar una sintaxis camelCase en los que tienen varias palabras en su nombre (por ejemplo “val-rangoFechas”).

Al incluir el script en nuestras páginas automáticamente dispondremos de un objeto campusMVP.Validador que ofrece tres métodos:

  • validaCampo(): este método realiza la validación de un campo que se le pase como parámetro, usando para ello los atributos anteriores. Como segundo parámetro opcional toma un booleano que sirve para indicar si queremos que se muestre automáticamente el mensaje de validación (true) o no (false). Además añade una propiedad al campo (elemento input) que se esté validando llamada esValido. Si la validación ha tenido éxito esta propiedad será true y sino será false. De esta forma, en caso de ser necesario, es fácil identificar luego los campos validados correctamente de los que no pasan la validación.
  • validaFormulario(): este método es igual al anterior, pero valida un formulario completo, campo a campo, usando para ello el método anterior. También toma un segundo parámetro booleano opcional que indica si se debe mostrar el mensaje de validación o no.
  • getMensajeValidacion(): toma como parámetro un campo y nos devuelve el mensaje de validación que tiene asociado, en caso de haber alguno. Este mensaje es el que tenga asignado el campo en el atributo val-mensaje.

Combinando estas tres simples funciones podemos conseguir diversos estilos de validación de manera sencilla. En el caso más simple, con una sola llamada podremos validar el formulario completo y mostrar al usuario los mensajes pertinentes.

Cuando se elije la opción de mostrar los mensajes de validación, el código añade automáticamente un elemento <span> como último hijo del elemento que contiene al campo (generalmente un control <label> asociado a éste) al cual se le asocia automáticamente la clase CSS llamada .val-mensaje. Además, al campo que se está validando se le añade automáticamente la clase .val-NoValido. De este modo es muy sencillo personalizar el aspecto de los mensajes de validación y de los campos que no pasan la validación, simplemente definiendo estas dos clases en las hojas de estilo asociadas a la página.

A continuación tienes un par de vídeos explicativos:

  • El primero muestra el uso práctico de la biblioteca con varias modalidades diferentes de validación (todo junto al final, por cada campo individual al perder el foco, o usando un resumen de validación).
  • El segundo es una explicación del código que muestra cómo está hecho el módulo.

DEMO: Uso práctico de la biblioteca de validación

Web con formulario:

<!DOCTYPE html>
<html>
<head>
<title>Validación totalmente automática al enviar</title>
<style type="text/css">
	body 
	{
		font-family:"Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
		font-size:12px;
	}
	.cabecera
	{
		text-align:center;
		font-weight:bold;
		font-size:2.5em;
		color:blue;
	}
	.Encabezado
	{
		font-weight:bold;
		font-size:1.5em;
	}
	.Obligatorio
	{
		color:red;
	}
}
</style>
 
<link rel="stylesheet" href="validador.css"></link>
    <script type="text/javascript" src="campusMVP_Validador.min.js"></script>
<script type="text/javascript">
    function miValidacionEspecial(marcadoSiONo) {
        marcadoSiONo = new Boolean(marcadoSiONo);
        return (this.checked == marcadoSiONo);
    }
</script>
</head>
<body>
 
<h1 class="cabecera">Env&iacute;o de datos personales</h1>
 
<form name="frmDatos" action="" method="get" onsubmit="return campusMVP.Validador.validaFormulario(this, true);">
<label for="txtNombre" class="Encabezado">Nombre<span class="Obligatorio">*</span>: 
	  <input type="text" name="txtNombre" size="20" val-obligatorio val-longMin="2" val-longMax="20" val-mensaje="El nombre debe tener al menos 2 letras, y 20 como máximo.">
</label>
<br/>
<label for="txtApellidos" class="Encabezado">Apellidos<span class="Obligatorio">*</span>: 
    <input type="text" name="txtApellidos" size="40" val-obligatorio val-longmin="2" val-longmax="40" val-mensaje="El apellido debe tener al menos 2 letras, y 40 como máximo.">
</label>
<br/>
<label for="txtFNac" class="Encabezado">Fecha Nacimiento<span class="Obligatorio">*</span>: 
	  <input type="text" name="txtFNac" size="10" val-obligatorio val-fecha val-rangoFechas="1/1/1900" val-mensaje="La fecha debe estar entre 1900 y 1997"> 
</label>
<label for="txtCP" class="Encabezado"> C.P.: 
    <input type="text" name="txtCP" size="5">
</label>
<br/>
<label for="txtEdad" class="Encabezado">
    Edad (18-100):
    <input type="text" name="txtEdad" size="2" val-rango="18-100" val-mensaje="La edad debe estar entre 18 y 100 años">
</label>
<br />
<label for="txtTlfn" class="Encabezado"> Teléfono: 
	<input type="text" name="txtTlfn" size="15">
</label>
<br/>
<label for="txtEMail" class="Encabezado"> eMail: 
	<input type="text" name="txtEMail" size="30" val-EMail val-longMax="30" val-mensaje="Introduce un email válido">
</label>
<br/>
<p> 
  <input type="checkbox" name="chkInfo" value="checkbox" checked val-custom="miValidacionEspecial(true)" val-mensaje="Debes aceptar recibir información ;-).">
  <label for="chkInfo" class="Encabezado">Deseo recibir informaci&oacute;n sobre sus productos</label>
</p>
<p> 
  <input type="submit" name="Enviar" value="Enviar datos personales">
  <input type="reset" name="Reset" value="Volver a rellenar datos">
</p>
</form>
</body>
</html>

Vemos que se han añadido en los campos una serie de atributos que comienzan por val que nos sirven para definir cómo queremos validar cada campo.

En este otro formulario HTML la validación se realiza al perder el foco en un campo:

<!DOCTYPE html>
<html>
<head>
<title>Validación totalmente automática individual al perder foco</title>
<style type="text/css">
	body 
	{
		font-family:"Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
		font-size:12px;
	}
	.cabecera
	{
		text-align:center;
		font-weight:bold;
		font-size:2.5em;
		color:blue;
	}
	.Encabezado
	{
		font-weight:bold;
		font-size:1.5em;
	}
	.Obligatorio
	{
		color:red;
	}
}
</style>
 
<link rel="stylesheet" href="validador.css"></link>
    <script type="text/javascript" src="campusMVP_Validador.min.js"></script>
<script type="text/javascript">
    function miValidacionEspecial(marcadoSiONo) {
        marcadoSiONo = new Boolean(marcadoSiONo);
        return (this.checked == marcadoSiONo);
    }
</script>
</head>
<body>
 
<h1 class="cabecera">Env&iacute;o de datos personales</h1>
 
<form name="frmDatos" action="" method="get" onsubmit="return campusMVP.Validador.validaFormulario(this, true);">
<label for="txtNombre" class="Encabezado">Nombre<span class="Obligatorio">*</span>: 
	  <input type="text" name="txtNombre" size="20" val-obligatorio val-longMin="2" val-longMax="20" val-mensaje="El nombre debe tener al menos 2 letras, y 20 como máximo." onblur="campusMVP.Validador.validaCampo(this, true);">
</label>
<br/>
<label for="txtApellidos" class="Encabezado">Apellidos<span class="Obligatorio">*</span>: 
    <input type="text" name="txtApellidos" size="40" val-obligatorio val-longmin="2" val-longmax="40" val-mensaje="El apellido debe tener al menos 2 letras, y 40 como máximo." onblur="campusMVP.Validador.validaCampo(this, true);">
</label>
<br/>
<label for="txtFNac" class="Encabezado">Fecha Nacimiento<span class="Obligatorio">*</span>: 
    <input type="text" name="txtFNac" size="10" val-obligatorio val-fecha val-rangofechas="1/1/1900" val-mensaje="La fecha debe estar entre 1900 y 1997" onblur="campusMVP.Validador.validaCampo(this, true);"> 
</label>
<label for="txtCP" class="Encabezado"> C.P.: 
    <input type="text" name="txtCP" size="5">
</label>
<br/>
<label for="txtEdad" class="Encabezado">
    Edad (18-100):
    <input type="text" name="txtEdad" size="2" val-rango="18-100" val-mensaje="La edad debe estar entre 18 y 100 años" onblur="campusMVP.Validador.validaCampo(this, true);">
</label>
<br />
<label for="txtTlfn" class="Encabezado"> Teléfono: 
	<input type="text" name="txtTlfn" size="15">
</label>
<br/>
<label for="txtEMail" class="Encabezado"> eMail: 
    <input type="text" name="txtEMail" size="30" val-email val-longmax="30" val-mensaje="Introduce un email válido" onblur="campusMVP.Validador.validaCampo(this, true);">
</label>
<br/>
<p> 
    <input type="checkbox" name="chkInfo" value="checkbox" checked val-custom="miValidacionEspecial(true)" val-mensaje="Debes aceptar recibir información ;-)." onblur="campusMVP.Validador.validaCampo(this, true);">
  <label for="chkInfo" class="Encabezado">Deseo recibir informaci&oacute;n sobre sus productos</label>
</p>
<p> 
  <input type="submit" name="Enviar" value="Enviar datos personales">
  <input type="reset" name="Reset" value="Volver a rellenar datos">
</p>
</form>
</body>
</html>

DEMO: Explicación del código de la biblioteca de validación

Este es el contenido del fichero campusMVP_Validador.js:

///////////////////////////////////////////////////////////////////////////////////
// Biblioteca para validación automática de campos de formularios Web
// Creada por José Manuel Alarcón (www.jasoft.org)
// como ejemplo del curso "JavaScript profesional para desarrolladores y diseñadores web"
// de campusMVP (www.campusmvp.es)
//
// Licencia Creative Commons - CC BY (http://creativecommons.org/licenses/by/4.0/)
///////////////////////////////////////////////////////////////////////////////////
(function() {
 
    //Prefijo de los atributos de validación
    var PREFIJO = "val-";
 
    //Función que valida todos los campos de un formulario dado, verificando las reglas propias creadas con atributos
    //frm: el formulario a validar
    //mostrarMensajes: un booleano que indica si se debe mostrar o no la interfaz de validación por defecto. Si no se especifica no se mostrará.
    function _validaFormulario(frm, mostrarMensajes) {
        //Me aseguro de que el segundo parámetro tiene un booleano válido
        mostrarMensajes = !!mostrarMensajes;
 
        var res = true; //supongo éxito en la validación
        for (var i = 0; i < frm.elements.length; i++) {
            var campo = frm.elements[i];
            if (!_validaCampo(campo, mostrarMensajes)) { //Validamos cada campo individualmente
                //Si la validación del campo ha fallado
                //devolvemos falso como resultado de la validación
                res = false;
            }
        }
        //Añadimos o cambiamos la propiedad "esValido" del formulario, para indicar si ha validado el formulario completo o no
        frm.esValido = res;
        return res;
    };
 
    //Función que valida un campo determinado que se le pase como parámetro
    function _validaCampo(campo, mostrarMensaje) {
        // La propiedad estándar del DOM 'atributes' nos devuelve todos
        // los atributos que tiene un elemento HTML, tanto los estándar
        // como los que nosotros añadamos (como los que empiezan por 'val'
        var atributos = campo.attributes;
        //Recorro los atributos del campo y verifico los que empiecen por "val-" si los hay
        for (var i = 0; i < atributos.length; i++) {
            var nombre = atributos[i].name.toLowerCase();
            if (nombre.indexOf(PREFIJO) == 0) //si empieza por el prefijo de los atributos de validación, es que es un atributo de validación y lo verificamos
                if (!_verificaAtributo(campo, atributos[i])) { //En caso de fracaso de la validación
                    //Si así se ha indicado, mostramos el mensaje asociado
                    if (mostrarMensaje) _muestraMensajeValidacion(campo);
                    //Marcamos el campo como no válido con la propiedad "esValido"
                    campo.esValido = false;
                    return false; //Si no tiene éxito la validación, indicamos el fracaso en el resultado
                    //Salimos del bucle: ya no seguimos validando el resto de atributos del mismo campo (con que haya uno no válido, se muestra el mensaje)
                } else {
                    //Si se ha validado correctamente quitamos el mensaje de validación en caso de estar habilitado
                    if (mostrarMensaje) _quitaMensajeDeValidacion(campo);
                    //Marcamos el campo como válido con la propiedad "esValido"
                    campo.esValido = true;
                }
        }
        campo.esValido = true;
        return true;
    };
 
 
    //Verifica un determinado atributo de validación para un determinado campo
    function _verificaAtributo(campo, atributo) {
        //Llamo a la función apropiada en función del atributo que sea. Esto permite ampliar fácilmente los atributos de validación
        //No depende de mayúscula ni minúscula
        switch (atributo.name.toLowerCase()) {
            //Obligatorio
            case "val-obligatorio":
                return _esObligatorio(campo);
                break;
            case "val-longmin":
                return _longMinima(campo, atributo.value);
                break;
            case "val-longmax":
                return _longMaxima(campo, atributo.value);
                break;
            case "val-num":
                return _esNumero(campo);
                break;
            case "val-entero":
                return _esNumeroEntero(campo);
                break;
            case "val-fecha":
                return _esFecha(campo);
                break;
            case "val-rango":
                return _verificarRango(campo, atributo.value);
                break;
            case "val-rangofechas":
                return _verificarRangoFechas(campo, atributo.value);
                break;
            case "val-email":
                return _verificarEmail(campo);
                break;
            case "val-custom":
                return _llamarFuncionPersonalizada(campo, atributo.value); //Aseguramos que se devuelve un booleano
                break;
            default:
                //Parámetro no reconocido o parámetro informativo (como "val-mensaje"): NO hacemos nada
                return true;
        }
    };
 
    //////////////////////////////////////////////////////////////////////////////////////////////////////
    // FUNCIONES DE VALIDACIÓN
    // Simplemente validan los campos y devuelve true o false según se haya pasado o no la validación.
    // No interfieren con la interfaz de usuario
    //////////////////////////////////////////////////////////////////////////////////////////////////////
 
    //Funcion que verifica si un campo del formulario es obligatorio o no
    function _esObligatorio(campo) {
        var valor = campo.value.trim();
        return (valor != ""); //devuelve true si no está vacío (no cuenta espacios al principio ni al final)
    };
 
    //Función que verifica que el campo tenga un valor entero con una longitud mínima
    function _longMinima(campo, valor) {
        valor = parseInt(valor, 10); //El valor del atributo de validación debe ser numérico
        return (!isNaN(valor) && campo.value.length >= valor);
    };
 
    //Función que verifica que el campo tenga un valor entero que no supere una longitud máxima
    function _longMaxima(campo, valor) {
        valor = parseInt(valor, 10); //El valor del atributo de validación debe ser numérico
        return (!isNaN(valor) && campo.value.length <= valor);
    };
 
    //Función que verifica si el valor de un campo es numérico o no
    function _esNumero(campo) {
        var valor = parseFloat(campo.value);
        if (isNaN(valor)) return false; //Si no es un número ya no valida
        //Si es un número aún tiene que coincidir con el valor del campo (la covnersión hace caso omiso de los valores no válidos, por lo que no llega con ver si es numérico).
        return (valor.toString() == campo.value.trim());
    };
 
    //Función que verifica si el valor de un campo es un número entero o no
    function _esNumeroEntero(campo) {
        var valor = parseInt(campo.value, 10);
        if (isNaN(valor)) return false; //Si no es un número ya no valida
        //Si es un número aún tiene que coincidir con el valor del campo (la covnersión hace caso omiso de los valores no válidos, por lo que no llega con ver si es numérico).
        return (valor.toString() == campo.value.trim());
    };
 
    //Función que verifica si el valor de un campo es una fecha o no
    function _esFecha(campo) {
        var valor = _convertirAFecha(campo.value);
        return !isNaN(valor);
    };
 
    //Verifica que el campo tenga un valor comprendido en el rango que se especifique, del tipo "0-100"
    function _verificarRango(campo, valor) {
        //Si está vacío no se verifica
        if (campo.value == "") return true;
        var rango = valor.split("-");
        if (rango.length != 2) return false; //debe haber dos valores
        var min = parseFloat(rango[0]); //Los valores deben ser numéricos (valen decimales)
        if (isNaN(min)) return false;
        var max = parseFloat(rango[1]);
        if (isNaN(max)) return false;
        //El valor del campo debe ser un número (esto es implícito a este tipo de validación)
        var val = parseFloat(campo.value);
        if (isNaN(val)) return false;
        //La validación propiamente dicha
        return (val >= min && val <= max);
    };
 
    //Verifica un rago de fechas separadas con guiones
    //Si solo se especifica uno, se comprueba solo el rango inferior
    function _verificarRangoFechas(campo, valor) {
        //Si está vacío no se verifica
        if (campo.value == "") return true;
        var rango = valor.split("-");
        var min = _convertirAFecha(rango[0]); //Los valores deben ser fechas
        var max = null; //El rango superior no es obligatorio
        if (isNaN(min)) return false;
        if (rango.length > 1) {
            max = _convertirAFecha(rango[1]);
            if (isNaN(max)) return false; //Si esta debe ser válido
        }
        //El valor del campo debe ser una fecha (esto es implícito a este tipo de validación)
        var val = _convertirAFecha(campo.value);
        if (isNaN(val)) return false;
        //La validación propiamente dicha
        if (max != null) {
            return (val >= min && val <= max);
        } else {
            return (val >= min);
        }
    };
 
    //Verifica que el campo contenga un email válido
    function _verificarEmail(campo) {
        //Si está vacío no se verifica
        if (campo.value == "") return true;
        var valor = campo.value.trim();
        //Usaremos una expresión regular para validar que es un email. 
        //La mayor parte de los emails se pueden validar así, pero no todos: http://www.regular-expressions.info/email.html
        return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(valor);
    };
 
    //Esta función ejecuta el código que se indica como función de validación personalizada. 
    //La función debe estar declarada globalmente
    //Para facilitar su uso la ejecuta con el contexto (this) apuntando al campo que se está verificando
    //Detecta los posibles parámetros (de haberlos) y se los pasa a la función indicada (como cadenas, OJO).
    function _llamarFuncionPersonalizada(campo, valor) {
        //Si está vacío no se verifica
        if (campo.value == "") return true;
        //Analizamos la expresión, si es una función con paréntesis, separamos su nombre y sus parámetros
        //Usaré simples funciones de cadena para ello
        if (valor.length < 0) return false;
        var nomFunc, parametros = [];
        var posParentesis1 = valor.indexOf("("),
            posParentesis2 = valor.indexOf(")");
        if (posParentesis1 >= 0 && posParentesis2 >= 0 && posParentesis2 > posParentesis1) {
            nomFunc = valor.slice(0, posParentesis1);
            parametros = valor.slice(posParentesis1 + 1, posParentesis2).split(",");
        } else {
            nomFunc = valor;
        }
        //Verifico que es un simple nombre de función y que existe en el contexto global
        if (window[nomFunc])
            return window[nomFunc].apply(campo, parametros); //Se le pasa el campo como contexto
        else
            return false;
    };
 
    ///////////////////////////////////////////////////
    // FUNCIONES AUXILIARES
    ///////////////////////////////////////////////////
 
    //Función auxiliar para ayudar a convertir fechas en texto a verdaderas fechas de JavaScript
    //Espera fechas en formato DD/MM/YY o DD/MM/YYYY (o sea, en formato Español).
    function _convertirAFecha(strFecha) {
        return new Date(strFecha.split('/').reverse().join('/')); //Le damos la vuelta para que se interprete bien
    };
 
    //Añade una clase a las clases CSS aplicadas a un elemento
    //Si están soportadas usa las funciones estándar del W3C, sino lo hace "a pelo"
    function _addClass(elemento, clase) {
        if (elemento.classList) {
            elemento.classList.add(clase);
        } else {
            var re = new RegExp("\\b" + clase + "\\b");
            if (!re.test(elemento.className)) //Si NO tiene la clase aplicada, se la añadimos
                elemento.className += " " + clase;
        }
    };
 
    //Quita una clase de las clases CSS aplicadas a un elemento
    function _removeClass(elemento, clase) {
        if (elemento.classList) {
            elemento.classList.remove(clase);
        } else {
            var re = new RegExp("\\b" + clase + "\\b");
            //Le quitamos la clase si es necesario
            elemento.className.replace(re, "");
        }
    };
 
    //Extrae el mensaje asociado (de haberlo) a partir de un campo (val-mensaje)
    function _extraerMsgCampo(campo) {
        if (campo.attributes["val-mensaje"])
            return campo.attributes["val-mensaje"].value;
        else
            return ""; //Si no hay mensaje asociado, por defecto va en blanco (podríamos poner cualquier otra cosa, por ejemplo un asterisco
    }
 
    //Se encarga de crear la etiqueta con el mensaje de validación como último elemento del padre del campo validado
    function _muestraMensajeValidacion(campo) {
 
        //Se extrae el mensaje asociado al campo (de haberlo)
        var msgVal = _extraerMsgCampo(campo);
 
        //Marcamos el campo con la clase CSS "val-NoValido"
        _addClass(campo, "val-NoValido");
 
        //Si no está ya, añado una etiqueta con la información de error
        if (campo.parentNode.getElementsByClassName("val-mensaje").length == 0) {
            var spanMsg = document.createElement("span");
            spanMsg.className = "val-mensaje";
            spanMsg.textContent = msgVal;
            campo.parentNode.appendChild(spanMsg);
        }
    };
 
    //Retira las muestras visbles de que el campo no ha validado correctamente
    function _quitaMensajeDeValidacion(campo) {
        _removeClass(campo, "val-NoValido");
        //Quitamos los mensajes (de haberlos)
        var spansMsg = campo.parentNode.getElementsByClassName("val-mensaje");
        for (var i = 0; i < spansMsg.length; i++) {
            campo.parentNode.removeChild(spansMsg[i]);
        }
    };
 
    ///////////////////////////////////////////////////
    // OBJETO QUE ENCAPSULA LA FUNCIONALIDAD
    ///////////////////////////////////////////////////
    //Si no existe el "espacio de nombres" campusMVP, lo creamos
    if (!window.campusMVP) window.campusMVP = {};
    //Si no existe ya un objeto "Validador" en la ventana, lo asigno, con los métodos auxiliares necesarios
    if (!window.campusMVP.Validador) {
        window.campusMVP.Validador = {
            validaCampo: _validaCampo,
            validaFormulario: _validaFormulario,
            getMensajeValidacion: _extraerMsgCampo
        };
    }
})();

Creación de eventos propios

No es frecuente tener que hacerlo, pero en JavaScript es posible también definir eventos propios que luego se comportarán como cualquier otro evento, pudiendo gestionarlos de la manera convencional, con addEventListener y removeEventListener.

Por ejemplo, podemos hacer que, en un cuadro de texto, cuando el usuario inserte una dirección de email válida, se genere un evento emailInserted que se pueda gestionar por parte de nuestro código o el de otros.

Se trata de un ejemplo bastante trivial, pero si estamos creando un componente reutilizable, el uso de eventos nos permite desacoplar la gestión de determinados sucesos que ocurran en nuestro código del código que los va a gestionar, que ni siquiera tiene por qué ser nuestro, de modo que además pueda ser gestionado simultáneamente por varios “consumidores” diferentes (ya que pueden añadirse varios gestores con addeventListener).

Luego veremos un ejemplo real práctico que nos permite extender la funcionalidad del navegador mediante eventos. Pero antes, vamos a conocer la teoría sobre cómo hacerlo.

Crear un evento

Para crear nuestro propio evento solo tenemos que instanciar un nuevo objeto de la clase Event o de la clase CustomEvent.

En el primer caso solamente le pasaremos el nombre del evento al constructor, así:

var miEvento = new Event('emailInserted');

Mientras que en el segundo caso, con CustomEvent, podemos detallar más las propiedades de dicho evento, como por ejemplo si va a converger (“burbujear”) a través del DOM o no y, sobre todo, le podremos pasar información personalizada sobre el evento al código que lo gestione.

Por ejemplo, en nuestro evento emailInserted podríamos querer que no converja, o sea, que solo salte en el control que lo produce, y además queremos pasarle ya el email al manejador del evento para que lo pueda obtener de manera directa. Haríamos así:

var miEvento = new CustomEvent('emailInserted', {
    bubbles: false,
    detail: { email: eMailObtenidoDelInput}
});

Como vemos, el constructor, aparte del nombre del evento, toma un segundo parámetro con un objeto anónimo que se usará para establecer información sobre el evento concreto que estamos lanzando. En ese caso indicamos que no debe converger (bubbles: false) y además en su propiedad detail establecemos un objeto anónimo con una propiedad email a la que le asignaremos la dirección de email obtenida desde el cuadro de texto (no se muestra aquí cómo obtenerla por claridad y porque es trivial).

Las propiedades disponibles para el evento personalizado son las mismas que para cualquier evento convencional y además tiene la propiedad detail para permitirnos el envío de cualquier información concreta sobre el evento que nos interese, como acabamos de ver.

Aparte de bubbles se puede establecer si el evento será cancelable o no (cancelable), y en realidad, lo normal es que dejemos que las demás propiedades (como type, currentTarget o type) se rellenen automáticamente.

Lanzar un evento personalizado

Vale, ya sabemos cómo crear un evento, pero ¿cómo lo lanzamos? La respuesta es que, al igual que cada elemento de la página dispone de los métodos addEventListener y removeEventListener, la infraestructura de eventos del navegador también los dota de un método específico llamado dispatchEvent().

En realidad, todos los elementos de una página que pueden gestionar eventos implementan una interfaz denominada EventTarget. Si un elemento implementa esta interfaz quiere decir que dispone de estos tres métodos.

Este método simplemente toma el evento que queremos lanzar y lo notifica, forzando al navegador a llamar a todos los manejadores que se le hayan asociado usando addEventlistener.

Por ejemplo, en el caso de nuestro hipotético <input> que notificaba cuando se ha introducido una dirección de email, podríamos capturar los cambios de contenido en el evento change convencional del cuadro de texto y cuando detectemos que lo que hay escrito es un email válido, definimos el evento como acabamos de ver y lo lanzamos desde el manejador del evento change con una única instrucción, así:

miInput.dispatchEvent(miEvento);

siendo miEvento el que hemos definido un poco más arriba.

Si alguien lo quiere capturar lo único que tendría que hacer es usar un addEventListener, de la manera habitual, en este caso aplicado al elemento <input> que lo tenga definido, así:

var miInput = document.getElementById('CampoEmail');
miInput.addEventListener('emailInserted', function(ev) {
    console.log('Se ha introducido el email %s en el cuadro de texto', ev.detail.email);
});

Como vemos, se gestiona como cualquier otro evento, y en este caso además podemos obtener cierta información extra (el email introducido) recurriendo a la propiedad detail del parámetro con información del evento, que se estableció al definir el CustomEvent, antes de lanzarlo.

Una cuestión importante de este tipo de eventos personalizados es que cuando llamamos a dispatchEvent() desde nuestro código, todos los manejadores que haya asociados al evento en cuestión se lanzan síncronamente. Esto quiere decir que, hasta que se hayan ejecutado todos no se devuelve el control del código a la línea siguiente, por lo que si tenemos algo así:

miInput.dispatchEvent(miEvento);
console.log('Se han ejecutado todos los manejadores');

No veríamos el mensaje en consola hasta que se hayan ejecutado todos. Debemos tenerlo en cuenta.

Valor devuelto por un evento

La función dispatchEvent() espera un valor de retorno que podemos capturar. Si el evento que estamos lanzando es cancelable y alguno de los manejadores asociados con addEventListener llama a cancelDefault() o devuelve false, entonces el resultado de dispatchEvent() será false también. En cualquier otro caso será true.

Por lo tanto, si nuestro código debe hacer algo tras el evento que queremos que se pueda cancelar, debemos comprobar el valor devuelto y si es false, actuar en consecuencia.

Por ejemplo, imagina que en nuestro ejemplo del <input> queremos que el evento emailInserted sea cancelable, anulando el email en caso de cancelarlo. En el caso de que algún manejador cancele el valor por defecto (dispatchEvent() devolvería false), lo que haríamos sería borrar el contenido del cuadro de texto porque no se permite el email introducido.

Este ejemplo sencillo del evento emailInserted creo que es un buen ejercicio inicial sobre eventos propios. Te recomiendo, antes de continuar con el resto del módulo, que intentes reproducirlo por tu cuenta en una página que tenga algún control de tipo <input>. Luego intenta encapsularlo en un archivo .js de modo que, al añadir la referencia al archivo en un <script>, puedas añadir el nuevo evento a cualquier <input> de tipo texto llamando por ejemplo a una función addEmailInsertedEvent.

Ejemplo: eventos para detectar la aparición y desaparición de elementos en la página

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, tanto total como parcialmente.

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 poder 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 eventos para los elementos que nos informasen de cuándo aparecen o desaparecen de la parte visible de la página. Nos suscribiríamos a este evento de la manera convencional 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 crear alguno propio para poder disponer de una funcionalidad similar a esta.

En esta lección y un próximo vídeo práctico vamos a aprovechar lo que conocemos de creación de eventos para desarrollar desde cero la funcionalidad necesaria para conseguir tener varios eventos relacionados con la visibilidad, listos para ser utilizados en cualquier página. Nos servirá de práctica de varios de los conceptos aprendidos a lo largo del curso.

En concreto vamos a crear 4 eventos para cualquier elemento de una página relacionados con su visibilidad, a saber:

  • show: que se lanza cuando un elemento aparece por completo (no solo una parte) en el viewport de la página. Si el elemento está ya visible por completo inicialmente no se notifica el evento. Solo se notifica cuando cambia el estado de visibilidad, es decir, no está visible por completo y pasa a estarlo.
  • hide: se lanza cuando el elemento pasa a estar completamente fuera del área visible.
  • showpart: se lanza cuando el elemento estaba oculto por completo y cualquier parte del mismo entra en el área visible, es decir, se muestra parcialmente tras haber estado oculto.
  • hidepart: se lanza cuando el elemento estaba completamente visible y cualquier parte del mismo sale de la zona visible, es decir, cuando se oculta, aunque sea una fracción pequeña del mismo.

Los eventos añaden una propiedad visible al objeto estándar details del objeto event del manejador del evento, de modo que puedas usar el mismo manejador para gestionar varios de ellos (por ejemplo, el mismo para show y hidepart que tenderán a ir relacionados).

En esta lección vamos a ver algunos detalles generales de la “fontanería” necesaria para la implementación y luego en el vídeo asociado veremos cuestiones específicas sobre cómo generar y gestionar esos eventos, además de cómo usar la biblioteca.

Esta biblioteca de eventos la he creado como un proyecto Open Source que puedes encontrar en GitHub, no solo como un ejemplo del curso. Por ello, su documentación en Github al igual que sus comentarios están en inglés. Lo he incluido en los ejemplos del ZIP de este módulo por lo que si lo prefieres puedes verlo y usarlo directamente desde ahí, aunque en GitHub también hay una versión minimizada que ocupa mucho menos, pero a efectos de aprendizaje debes usar la que tiene todos los comentarios.

La funcionalidad la vamos a dividir en varios bloques independientes 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 la 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);

En este caso se mostraría en la consola la posición del borde superior del elemento respecto a la parte de arriba del viewport.

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, al igual que con otras acciones posibles en la página por parte del usuario. 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 por completo dentro de la página en un momento dado definiendo una función como esta:

function isElementTotallyVisible(elt) {
    var viewportWidth = window.innerWidth || document.documentElement.clientWidth;
    var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    //Posición de la caja del elemento
    var box = elt.getBoundingClientRect();
    return ( box.top >= 0 &&
             box.bottom <= viewportHeight &&
             box.left >= 0 &&
             box.right <= viewportWidth );
}

Lo que hacemos es, antes de nada, determinar el ancho y el alto del área visible actual, para lo cual usamos la propiedad innerWidth e 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). Si no te interesa puedes obviarlo y dejar solo las innerX. getBoundingClientRect() está soportado incluso en versiones muy antiguas de IE porque es un invento inicial de Microsoft que luego incorporaron los demás navegadores.

A continuación se obtienen los límites de la caja del elemento, y se comparan 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 cuándo cualquier parte del elemento se sale de la vista (que sería lo anterior) sino cuándo se sale 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(elt) {
    var viewportWidth = window.innerWidth || document.documentElement.clientWidth;
    var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    //Posición de la caja del elemento
    var box = elt.getBoundingClientRect();
    var insideBoxH = (box.left >= 0 && box.left <= viewportWidth) ||
                      (box.right >= 0 && box.right <= viewportWidth);
    var insideBoxV = (box.top >= 0 && box.top <= viewportHeight) ||
                      (box.bottom>= 0 && box.bottom <= viewportHeight);
    return (insideBoxH && insideBoxV);
}

En este caso las condiciones son más complicadas, pero tampoco nada fuera de lo común. 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. Puede parecer lioso, por eso lo he puesto en la parte escrita del ejemplo, pero es fácil de ver si haces unas pruebas.

3.- Detectar automáticamente si el elemento aparece o desaparece del área visible

  IMPORTANTE: en este ejemplo utilizo eventos del navegador de forma que practicaquemos también su uso, pero no sería la manera más adecuada de hacer esto. Existe una API específica del navegador, la [[https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API|Intersection Observer API]], que sirve para hacer esto de forma eficiente.

Ahora que ya sabemos determinar si el elemento está total o parcialmente dentro del área visible, lo primero es determinar por qué motivos es posible que un elemento se desplace y, por lo tanto, pueda aparecer o desaparecer de dentro del área visible de la página.

Existen varias causas para la modificación de la posición de un elemento y por lo tanto para su posible cambio de visibilidad:

  • Scroll de la página. Este es el más evidente. Lo podemos detectar gracias al evento scroll del mismo nombre de la ventana.
  • 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. Así que, para evitar este posible efecto, lo detectaríamos en el evento load de la página y no en DOMContentLoaded.

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, y serán los que utilicemos.

4.- Determinar si el elemento ha aparecido o desaparecido del área visible

Vamos a definir una función que nos va a permitir determinar si un elemento está parcialmente en el área visible y poder ejecutar una función en dicho caso:

function inViewportPartially(elt, handler) {
    var prevVisibility = isElementPartiallyVisible(elt);
    //Se define un manejador que se llamarña ante posibles cambios
    function detectPossibleChange() {
        var isVisible = isElementPartiallyVisible(elt);
        if (isVisible != prevVisibility) { //ha cambiado el estado de visibilidad
            prevVisibility = isVisible;
            if (typeof handler == "function")
                handler(isVisible, elt);
        }
    }
 
    //Gestionar los eventos que nos interesan con la función anterior para notificar la detección
    window.addEventListener("load", detectPossibleChange);
    window.addEventListener("resize", detectPossibleChange);
    window.addEventListener("scroll", detectPossibleChange);
    return detectPossibleChange;    //Para poder luego retirar el manejador con removeEventLister
}

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 píxel 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 clausura a la que pertenece, al ser una función interna a la otra.

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.

Existe otra función gemela de esta, 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á que ha vuelto a aparecer. Es mucho más estricto que el evento anterior al tener que estar todo dentro de la página.

Bien, esto es el código de “fontanería” que no tiene que ver con la definición de eventos propios, sino con las cosas fundamentales que necesitamos para detectar los cambios de visibilidad. Ahora, en el vídeo práctico, veremos cómo implementar los 4 eventos propios que nos interesan, basándonos en todo esto. Asegúrate de haber entendido bien todo lo anterior antes de continuar.

Anexo: Otras formas de cambiar la disposición de la página

Además de los 3 eventos que hemos considerado para detectar cambios, existen otras maneras menos frecuentes de que se modifique la posición de los elementos de la página. Concretamente 3 casos que no vamos a utilizar en la biblioteca por no mermar el rendimiento de la página:

  • 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 relacionados (DOMNodeInserted() y DOMNodeRemoved()). Lo consideraremos un caso poco frecuente y no lo incluiremos en la biblioteca.
  • 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. Muy poco eficiente y malo para el rendimiento, y además si lo ocultamos así también sabremos cuando, por lo que no nos haría falta el evento tampoco. Lo dejamos fuera.
  • 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 puede hacer zoom con los dedos (si se le permite), 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.

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 usando el evento personalizado pertinente, así que en la práctica no compensa preocuparse por ellos salvo quizá en algún caso muy particular.

Funcionamiento práctico

DEMO: Explicación del código

Prácticas propuestas para el módulo

informatica/programacion/cursos/programacion_avanzada_javascript/eventos_navegadores.1728654246.txt.gz · Última modificación: por tempwin