Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:eventos_navegadores

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anteriorRevisión previa
Próxima revisión
Revisión previa
informatica:programacion:cursos:programacion_avanzada_javascript:eventos_navegadores [2024/10/11 15:09] – [DEMO: Uso práctico de la biblioteca de validación] tempwininformatica:programacion:cursos:programacion_avanzada_javascript:eventos_navegadores [2024/10/30 15:45] (actual) – [Prácticas propuestas para el módulo] tempwin
Línea 7: Línea 7:
 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. 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.
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:07-bang-gun.jpg |}}
-pistola +
-</WRAP>+
  
 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. 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.
Línea 414: Línea 412:
 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: 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:
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:07-flujo-eventos-dom.png |}}
-Flujo-Eventos +
-</WRAP>+
  
 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. 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.
Línea 428: Línea 424:
 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: 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:
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:07-fase-eventos-dom.png |}}
-fase-eventos +
-</WRAP>+
  
   * **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 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.
Línea 716: Línea 710:
 | ''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'' | | ''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.  | | ''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.  |+| ''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.  | | ''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 [[http://www.w3.org/TR/DOM-Level-2-Views/views.html|aquí]]. Solo se ofrece en eventos de interfaz de usuario, o sea, de tipo UIEvent.  | | ''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 [[http://www.w3.org/TR/DOM-Level-2-Views/views.html|aquí]]. Solo se ofrece en eventos de interfaz de usuario, o sea, de tipo UIEvent.  |
Línea 888: Línea 882:
 Además, otra página interesante para tener como referencia, aunque ya tiene unos años, es [[http://help.dottoro.com/|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): Además, otra página interesante para tener como referencia, aunque ya tiene unos años, es [[http://help.dottoro.com/|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):
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:07-dottoro.gif |}}
-Dottoro Buscar elementos en índice +
-</WRAP>+
  
 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. 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.
Línea 1651: Línea 1643:
 ==== Eventos de formularios y controles de entrada ==== ==== Eventos de formularios y controles de entrada ====
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:07-formulario-icono.jpg |}}
-formulario +
-</WRAP>+
  
 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''. 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''.
Línea 1851: Línea 1841:
  
 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. 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.
- 
-Y este es el contenido del fichero ''campusMVP_Validador.js'': 
- 
-<code javascript> 
-/////////////////////////////////////////////////////////////////////////////////// 
-// 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) { 
- 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 
- }; 
- } 
-})(); 
-</code> 
  
 En este otro formulario HTML la validación se realiza al perder el foco en un campo: En este otro formulario HTML la validación se realiza al perder el foco en un campo:
Línea 2251: Línea 1929:
 </code> </code>
 ==== DEMO: Explicación del código de la biblioteca de validación ==== ==== DEMO: Explicación del código de la biblioteca de validación ====
 +
 +Este es el contenido del fichero ''campusMVP_Validador.js'':
 +
 +<code javascript>
 +///////////////////////////////////////////////////////////////////////////////////
 +// 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
 +        };
 +    }
 +})();
 +</code>
  
 ===== Creación de eventos propios ===== ===== 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í:
 +
 +<code javascript>
 +var miEvento = new Event('emailInserted');
 +</code>
 +
 +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í:
 +
 +<code javascript>
 +var miEvento = new CustomEvent('emailInserted', {
 +    bubbles: false,
 +    detail: { email: eMailObtenidoDelInput}
 +});
 +</code>
 +
 +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 [[https://developer.mozilla.org/en-US/docs/Web/API/Event#Properties|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 ''[[https://developer.mozilla.org/en-US/docs/Web/API/EventTarget|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í:
 +
 +<code javascript>
 +miInput.dispatchEvent(miEvento);
 +</code>
 +
 +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í:
 +
 +<code javascript>
 +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);
 +});
 +</code>
 +
 +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í:
 +
 +<code javascript>
 +miInput.dispatchEvent(miEvento);
 +console.log('Se han ejecutado todos los manejadores');
 +</code>
 +
 +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.
 +
 +<WRAP center round info 60%>
 +Esta biblioteca de eventos la he creado como un proyecto Open Source que puedes [[https://github.com/jmalarcon/visibilityEvents.js|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.
 +</WRAP>
 +
 +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'':
 +
 +<code javascript>
 +var caja = elto.getBoundingClientRect();
 +console.log(caja.top);
 +</code>
 +
 +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:
 +
 +<code javascript>
 +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 );
 +}
 +</code>
 +
 +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.
 +
 +<WRAP center round info 60%>
 +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.
 +</WRAP>
 +
 +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:
 +
 +<code javascript>
 +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);
 +}
 +</code>
 +
 +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 ===
 +
 +<WRAP center round important 60%>
 +    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.
 +</WRAP>
 +
 +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:
 +
 +<code javascript>
 +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
 +}
 +</code>
 +
 +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).
 +
 +<WRAP center round info 60%>
 +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.
 +</WRAP>
 +
 +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. [[https://github.com/tombigel/detect-zoom|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 ===== ===== Prácticas propuestas para el módulo =====
  
 +ste módulo se ha centrado en las diversas maneras que tenemos desde JavaScript para gestionar interacciones de los usuarios con el navegador. Esto nos permitirá crear todo tipo de aplicaciones prácticas basadas en lo que hemos aprendido antes sobre el lenguaje. En definitiva, llevar lo aprendido más cerca de la práctica real. Aunque los ejemplos que hemos realizado son intencionadamente sencillos para no desenfocarnos de lo que se pretendía enseñar, ahora tienes prácticamente todas las herramientas que necesitas para crear cualquier aplicación web que puedas necesitar. Es cuestión de juntar las piezas.
 +
 +Para reforzar los conceptos de este módulo y practicar un poco más te propongo los siguientes ejercicios:
 +
 +  * En el ejemplo de la etiqueta que persigue al ratón mostrando sus coordenadas, haz que cuando esté cerca del borde derecho de la página, lo suficiente como para que tape todo o parte de la etiqueta, ésta se muestre hacia el otro lado, de modo que siempre quede dentro del área visible de la página y no salgan barras de scroll.
 +  * Añade a una página cualquiera (que tenga al menos varios elementos ''div'', ''input'' de tipo ''text'' y párrafos de texto) la capacidad de detectar la pulsación de determinadas teclas que actuarán como atajos o teclas rápidas para un hipotético programa que tienes en esta página. Por ejemplo, que se detecte la pulsación de la tecla F1 y que esto haga que se muestre un ''div'' con ayuda, o la combinación de teclas <kbd>CTRL</kbd> + <kbd>T</kbd> para hacer cualquier otra cosa (en nuestro ejemplo, simplemente mostrará un ''alert''). Debes asegurarte de un par de cosas:
 +    * Que las teclas rápidas se detecten pulsándolas cuando el foco esté en cualquier parte de la página. 
 +    * Que si el foco está en algún elemento que acepte entrada de texto (un ''input'' de tipo ''text'', por ejemplo) que no tengan efecto dichas teclas.
 +
 +Una pregunta importante: ¿Funcionan todas las combinaciones? Por ejemplo, realmente eres capaz de detectar y responder a <kbd>CTRL</kbd> + <kbd>T</kbd> siempre. Prueba con <kbd>CTRL</kbd> + <kbd>Q</kbd> a ver si pasa lo mismo...
 +
 +  * En el ejemplo de control básico de un marcianito que hemos desarrollado en este módulo, hemos visto que hay un pequeño problema: la imposibilidad de detectar la pulsación de dos teclas a la vez. El efecto de esto es que si queremos que el marcianito se mueva en diagonal, lo conseguiremos pero veremos que lo hace "a saltos" como si pulsásemos las dos flechas (por ejemplo abajo y a la izquierda) de manera alternativa, y no a la vez.
 +
 +Además, si esto fuese un juego real existiría otro problema más sutil pero importante: los movimientos no estarían sincronizados con el resto de movimientos de otros elementos del juego. Normalmente los juegos tienen un bucle de ejecución y redibujan todos los elementos del mismo a la vez a intervalos regulares (por ejemplo 40 veces por segundo). Así, cada 25 milisegundos se pinta cada marcianito, asteroide y demás elementos móviles autónomos reflejando su posición. Para los elementos que controla el usuario, si usamos los eventos del teclado, se moverían en un bucle diferente del resto del juego, y no necesariamente coincidente con los intervalos de refresco de los demás elementos. De entrada puede que no lo notásemos, pero generaría problemas y parpadeos indeseados.
 +
 +Entonces, ¿cómo se solucionan estos problemas si quisiésemos crear un juego con HTML y JavaScript?
 +
 +La solución consiste en detectar las pulsaciones del teclado, pero **en lugar de procesarlas lo que hacemos es anotarlas en algún otro lugar (en un buffer)**. Es decir, en algún objeto auxiliar se van anotando cada una de las pulsaciones relevantes del teclado que hace el usuario (evento ''keydown''), así como cuando deja de pulsar las teclas (''keyup''), pero no hacemos nada más que anotarlas. Es luego, en el bucle de redibujado de los elementos, en donde comprobamos ese buffer de pulsaciones y actuamos en consecuencia moviendo los objetos afectados. Con esto se consiguen procesar varias teclas a la vez (o tener la sensación de ello, más bien) y se consiguen movimientos más fluidos.
 +
 +Otra opción, incluso mejor, es anotar la posición de cada marcianito, modificándola en los eventos que se van produciendo, pero sin dibujarla, es decir, sin cambiar la posición real de los elementos móviles. Luego en el bucle de dibujado (un ''setInterval'') es cuando realmente se dibujan los objetos, pero no en el momento mismo de detectar la pulsación de la tecla.
 +
 +Como ejercicio especial (no lo hagas si no te ves con fuerzas) te propongo cambiar el código de la página del marcianito por completo y hacerlo siguiendo la técnica que te acabo de explicar. Además deberías crear una clase para almacenar la información del marcianito y así no tener que almacenar su posición (y otros datos que tuviese) en una estructura aparte para poder redibujarla.
 +
 +<WRAP center round tip 60%>
 +Nota: De todos modos si estás pensando en hacer un juego en JavaScript es mejor usar alguna biblioteca especializada que ya nos da estas cosas y muchas otras ya hechas (aunque conviene conocer sus fundamentos). Hay infinidad de ellas en el mercado, casi todas gratuitas y Open Source. Por ejemplo [[http://craftyjs.com/|Crafty]], [[http://darlingjs.github.io/|DarlingJS]], [[http://www.limejs.com/|LimeJS]] o [[http://impactjs.com/|ImpactJS]]. En cualquier caso, ten en cuenta que la creación de juegos es un mundo en sí mismo y necesitarás aprender muchas técnicas específicas más allá de dominar los fundamentos de JavaScript.
 +</WRAP>
 +
 +En los siguientes módulos del curso vamos a estudiar algunas cuestiones adicionales útiles, pero con lo explicado hasta ahora ya conoces todo lo necesario para poder programar con éxito con JavaScript en cualquier entorno, pero especialmente aplicaciones web de lado cliente basadas en navegadores. Solo es cuestión de práctica y plantearse cosas nuevas con las que experimentar y aprender. En la programación para navegadores es muy importante tener a mano una buena documentación para consulta (como [[http://help.dottoro.com/ljsdaoxj.php|Dottoro Web Reference]], la [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference|Mozilla Developer Network]] o la [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference|documentación de Microsoft]] al respecto), probar bien los desarrollos en todos los navegadores posibles y, sobre todo, adquirir experiencia poco a poco haciendo pequeños desarrollos y subiendo paulatinamente la dificultad.
 +
 +¡Ah! y muy importante: a quien tiene un martillo todo le parecen clavos, así que trata de acordarte de que muchas de las cosas que se te planteen quizá las puedas conseguir sin necesidad de escribir código (solo con CSS, por ejemplo). Y lo mismo te digo si vienes de jQuery: multitud de cosas sencillas que se tienden a hacer con jQuery por inercia, es posible hacerlas directamente en JavaScript con el mismo esfuerzo, consiguiendo páginas más ligeras y rápidas. ¡No te olvides!
 +
 +===== Recursos =====
 +
 +  * [[https://www.codeproject.com/Articles/878436/Lets-Write-Unobtrusive-JavaScript|Let's Write Unobtrusive JavaScript]]
informatica/programacion/cursos/programacion_avanzada_javascript/eventos_navegadores.1728652159.txt.gz · Última modificación: por tempwin