Editado 15.03.2010: He eliminado el soporte específico a navegadores no estándares. No obstante, en dichos navegadores seguirá funcionando gracias al código general para navegadores antiguos. Además, se ha añadido una función para facilitar el añadir eventos que sólo se ejecuten una vez (ver más abajo). Las descargas están actualizadas.
Siempre que me dedico a diseñar interfaces asistidas con javascript, me encuentro con el mismo problema: ¿cómo manejar los eventos eficientemente y mantener la compatibilidad multinavegador?
La solución sencilla, y la más compatible1 (DOM 0), consiste en los atributos de evento, como onclick u onmouseover, pero tiene serias limitaciones: sólo admite una función (aunque podemos concatenar varias), y si optamos por concatenar funciones ni podríamos eliminar alguna, aparte de que la cancelación de eventos con return se vuelve una pesadilla.
Sin embargo, los navegadores estándares (motores mozilla, webkit, khtml...) disponen de elemento.addEventListener( nombre_evento, función, capturar ), que simplifica mucho el proceso de asignar varias funciones y elegir si capturan el evento por completo con un booleano, junto con elemento.removeEventListener( nombre_evento, función, capturaba ) para borrar funciones asignadas al elemento.
Por otro lado, navegadores no estándares2 (motor trident) recurren a elemento.attachEvent( onevento, función ), que funciona como addEventListener pero con nombres de eventos de HTML, y no permite establecer si se captura el evento o no, y para eliminar la función del evento tenemos elemento.detachEvent( onevento, función).
Crear una función para simplificar el proceso de asignación y desasignación de funciones a eventos, que sirva para los tres casos anteriores, nos lleva a renunciar a la posibilidad de definir si el evento se captura al momento de asignarlo, pero aún queda la posibilidad de capturar el evento dentro de la función asignada.
Pero de nuevo, existe una forma estándar y varias no estándar de capturar el evento, además de que los navegadores más antiguos sólo lo detendrán si la función asignada retorna false. En este último caso tendremos la fortuna3 de que dichos navegadores tampoco aceptan las formas modernas de asignación.
Teniendo todo esto en cuenta, he malgastado unas preciosas horas de mi vida en diseñar tres útiles funciones auxiliares (que empezaré a usar en todos lados). Podéis descargar este mismo código (3.2 KiB) a o la versión minificada (1 KiB).
/* Mi función auxiliar preferida, recibe un nombre de propiedad
* y un objeto, y retorna true o false dependiendo de si existe en
* el objeto o no. Si no se recibe un objeto se asume window */
function def(p,o){return (typeof((o||window)[p])!="undefined");}
function attEvent(o,e,f){
/* Función de asignación de evento, recibe objeto, nombre de evento
* (p.ej. click, no onclick) y función de callback */
var w3="addEventListener",a="attachedEvents",cp="cancel",nf,of,nr;
if(def(w3,o)){o[w3](e,f,false);} // Estándar
else{ // Funcionalidad para navegadores antiguos
if(!def(a,o)){o[a]={};} // Objeto auxiliar
if(!def(e,o[a])){o[a][e]={};} // Objeto evento
if(!def(f,o[a][e])){ // No agregar dos veces la misma función
nf=function(){
/* Detección de funciones bloqueadas por detEvent:
* si ha sido bloqueada, no se ejecuta la función */
if(o[a][e][f]){f.apply(this,arguments);}
};
nr=function(v){
// Retorna false si se ha ejecutado stopEvent
return !(def(cp,(v||window.event))&&(v[cp]==true));
};
if(o["on"+e]){
/* Si hay funciones previamente asignadas, asignamos
* una función que las ejecutará junto con la nueva */
of=o["on"+e]; // Funciones previas
o["on"+e]=function(){
// Ejecutamos:
of.apply(this,arguments); // 1. funciones previas
nf.apply(this,arguments); // 2. nueva función
// Retornamos false si se ha ejecutado stopEvent
return nr.apply(this,arguments);
};
}
else{o["on"+e]=nf;} // Agregar la primera función
}
/* Asignamos 1 en la función para el evento, si el valor
* pasa a ser 0 (por detEvent), la función no se ejecutará */
o[a][e][f]=1;
}
}
function attEventOnce(o,e,f){
attEvent(o,e,function(){
f.apply(this,arguments);
detEvent(this,e,arguments.callee);
});
}
function detEvent(o,e,f){
/* Función de desasignación de evento, recibe objeto, nombre de
* evento, y función a eliminar (o bloquear en el último caso) */
var w3="removeEventListener",a="attachedEvents";
if(def(w3,o)){o[w3](e,f,false);} // Estándar
else if(def(a,o)&&def(e,o[a])&&def(f,o[a][e])){
// Modo para navegadores antiguos (no borra, sólo desactiva)
o[a][e][f]=0;
}
}
function stopEvent(e){
/* Función de detención de eventos, recibe la instancia del evento
* por parámetro. */
var w3="stopPropagation",mz="preventDefault",e=(e||window.event);
/* Para navegadores que cumplen los estándares */
if(def(w3,e)){e[w3]();}
/* Evitamos la acción por defecto (mozilla) */
if(def(mz,e)){e[mz]();}
/* Para navegadores antiguos y no estándares: establecemos cancel y
* cancelBubble a true, returnValue a false, y retornamos false
* por si queremos usarlo en eventos asignados directamente. */
return e.returnValue=!(e.cancel=e.cancelBubble=true);
}
Usando estos métodos, agregar varias funciones al mismo evento sería bastante simple.
attEvent(window,"load",function(){alert("Primera vez.");});
attEvent(window,"load",function(){alert("Segunda vez.");});
O incluso podríamos asignar una función que se desasignara a sí misma y detuviera el evento cuando se ejecutase.
attEvent(document,"click",function(e){Editado 15.03.2010: Ahora existe la posibilidad, a petición popular, de añadir eventos de un sólo uso de forma más sencilla: con attEventOnce.
alert("¡Cliclí por primera vez, capturado!");
// Desasignamos la propia función (callee) del elemento (this)
detEvent(this,"click", arguments.callee);
// Detenemos el evento
stopEvent(e);
});
attEventOnce(document,"click",function(){
alert("¡Cliclí una única vez (sin capturar)!");
});
Sí, ya sé que frameworks mastodónticos, como jQuery o Mootools, ya han ideado formas de hacer todo esto, listas para usar, pero ¿qué tiene eso de divertido?
- Hay que decirlo: Chrome "se olvida" de los onsubmit de los formularios.
- Es tan obvio cuál que, tal cual, obviaré su nombre.
- Muy relativo todo, eso sí.
Créditos:
- Mozilla Developer Center.
- Inspirado en el horrible código de Gavin Kistner.
0 comentarios:
Publicar un comentario