Ir directamente al contenido de esta página

codexexempla.org

Cómo mejorar la usabilidad de una tabla de datos por medio de JavaScript no obstrusivo (y no es zebra striping)

Tabla de contenidos

  1. Introducción
  2. Objetivos
  3. Solución
  4. Personalización
  5. Limitaciones
  6. Pruebas en navegadores

Introducción

En algunos casos nos encontramos con tablas de datos tremendamente extensas. Cuando llevamos un lapso variable de tiempo desplazando de la barra de scroll, podemos encontrarnos con que nuestra atención ha disminuido y no recordamos los encabezados de la tabla que dan sentido a las celdas que estamos leyendo. Así que volvemos al inicio de la tabla a refrescar nuestra memoria, y nos vemos obligados a buscar de nuevo la fila en la que nos encontrábamos. Ciertamente, es un proceso, cuando menos, tedioso.

Veamos si podemos hacer algo para mejorar esta situación, con un poco de JavaScript no obstrusivo.

Objetivos

Solución

El siguiente script muestra los encabezados bajo las celdas de una fila sobre la que el usuario pasa el ratón:


 01  function escuchar(){
 02      var filas = document.getElementsByTagName('tr');
 03      for (var i=1;i<filas.length;i++){
 04          filas[i].addEventListener('mouseover', resaltar_fila, false);    
 05          filas[i].addEventListener('mouseout', restaurar_fila, false);    
 06      }
 07  }
 08  
 09  function resaltar_fila(){
 10          if(this.parentNode=='[object HTMLTableElement]'){
 11              var tabla = this.parentNode;
 12          } else { 
 13              var tabla = this.parentNode.parentNode;
 14          }
 15          var celdas_th = tabla.getElementsByTagName('th');
 16          var encabezado_temp = document.createElement('tr');
 17          encabezado_temp.setAttribute('id','encabezado_ayuda');
 18          for (var j=0;j<celdas_th.length;j++){
 19              var celda_temp = encabezado_temp.appendChild(document.createElement('td'));
 20              celda_temp.appendChild(document.createTextNode(celdas_th[j].firstChild.nodeValue));
 21              celda_temp.setAttribute('class','recuerdo_cabecera');
 22          }
 23          this.parentNode.insertBefore(encabezado_temp,this.nextSibling);
 24          var celdas = this.getElementsByTagName('td');
 25          for (var k=0;k<celdas.length;k++){
 26              celdas[k].className += ' celda_resaltada';
 27          }
 28  }
 29  
 30  function restaurar_fila(){
 31      document.getElementById('encabezado_ayuda').parentNode.removeChild(document.getElementById('encabezado_ayuda'));
 32      var celdas = this.getElementsByTagName('td');
 33      for (var l=0;l<celdas.length;l++){
 34          celdas[l].className = celdas[l].className.replace(/celda_resaltada/,'');
 35      }
 36  }
 37  
 38  if (document.addEventListener){ window.addEventListener('load',escuchar,false); }
            

Vayamos paso a paso.

Primero, hay que establecer escuchas sobre las filas de las tablas, así que asigno a la variable filas la lista de nodos que son filas de tablas (línea 2). Inmediatamente después, por medio de addEventListener1, y un bucle asigno una escucha del evento mouseover y otra para mouseout para cada fila (líneas 4 y 5).

Ahora necesito guardar en otra variable los encabezados de la tabla. Pero antes, necesito apuntar a la tabla que contiene la fila que ha lanzado la función. Por ello, al principio de la función resaltar_fila() compruebo qué elemento es el padre de la fila; si es [object HTMLTableElement], asigno a la variable tabla el valor de this.parentNode (this hace referencia al elemento que ha lanzado la función); si no es así, a la variable le asigno el valor de this.parentNode.parentNode. ¿Por qué? Bueno, porque el script está pensado para una página programada en XHTML: revisando la especificación, un elemento tr puede aparecer directamente como hijo de table —en cuyo caso la tabla se seleccionaría por medio de this.parentNode—, o bien dentro de un elemento thead, tbody o tfoot —por lo que para hacer referencia a la tabla habría que subir un ancestro más en el DOM, es decir, this.parentNode.parentNode— (líneas 10 a 14).

Por desgracia, aquí es donde empiezan los problemas.

En el modelo del W3C, this hace referencia al objeto al que se ha asignado el evento que ha desencadenado una función. En el modelo de Microsoft, los manejadores de eventos no son métodos de los elementos de XHTML, sino que funcionan como una función global, por lo que hacen referencia al objeto window. Bueno, apretando los dientes, podría emplear un poco de código propietario, y emplear


    if(this='[object HTMLTableRowElement]'){
        var fila_activa = this;
    } else {
        var fila_activa = window.event.srcElement;
    }
            

para después emplear en el resto del script la variable.

Si éste fuese el único inconveniente, todavía podría haber seguido desarrollando la rama para Explorer. Pero no, lamentablemente.

El siguiente problema es el resultado que se obtiene en Explorer 6 con diversas alertas —después de las líneas alternativas anteriores— para intentar detectar el elemento que ha lanzado la función:

¿La conclusión? Que como no hay forma de diferenciar el [object] que hace referencia a un hipotético tbody del [object] que haría referencia a table, no hay forma de asignar a la variable tabla en nodo que necesitamos.

Así pues, confiando en que el script obviamente no limita la accesibilidad de la tabla, decido incluir al IE6 en la lista de navegadores limitados que no pueden comprender mis indicaciones junto a, por ejemplo, Netscape 4. En fin, no solemos poder elegir entre lo bueno y lo malo, sino entre grados de males menores… Continuemos.

En la línea 15 creo una variable con la que referencio la lista de th de la tabla, en la 16 creo una fila temporal en la que incluiré sus contenidos, y en la 17 le asigno un identificador a esta fila virtual.

Seguidamente, para cada th creo una celda temporal a la que asigno su contenido, que es celdas_th[j].firstChild.nodeValue —puesto que el texto de un elemento cuenta como un hijo—, y cada celda temporal la incluyo como hijo de la fila temporal que creé antes por medio de appendChild. Por último, a cada celda temporal le asigno una clase que he preparado en la hoja de estilo, para darles formato (líneas 18 a 22).

Bien, con todo esto, ya tengo lista mi fila con los encabezados que necesito, pero ahora tengo que indicar dónde quiero que aparezca esta fila.

Mi intención es que aparezca debajo de la fila que está consultando el usuario. Por desgracia, no existe en la especificación de DOM algo así como insertAfter, por lo que tengo que dar un pequeño rodeo.

Como lo que sí existe es insertBefore, en la línea 23 lo que hago es decirle al navegador «dentro del nodo que sea el padre del elemento que ha lanzado la función (this.parentNode) insértame mi nueva fila justo antes del elemento que sea el siguiente adyacente de ese mismo elemento (insertBefore(encabezado_temp,this.nextSibling))». Listo.

El las líneas de la 24 a la 27 lo único que hago es resaltar las celdas de la fila seleccionada con una clase que también he creado previamente en la hoja de estilo. El único detalle es que empleo el código


    celdas[k].className += ' celda_resaltada';
            

en lugar de


    celdas[i].setAttribute('class', 'celda_resaltada');
            

para no sustituir ningún estilo que puedan tener asociadas las celdas en cuestión por medio de un class; si lo hiciera así, cuando luego eliminase el estilo celda_resaltada en la función siguiente, la celda podría perder su aspecto original.

Y para finalizar, queda la función con la que elimino la fila añadida (líneas 30 a 36). No tiene ningún misterio, simplemente empleo removeChild y apunto a la fila por medio del id que le asigne en la función anterior. Para restablecer el aspecto de las celdas resaltadas, reemplazo en el atributo class de éstas el nombre de la clase añadida por una cadena vacía (línea 34). Si lo hiciera por medio de


    celdas[i].setAttribute('class', '');    
            

me vería obligado a guardar previamente en otra variable la posible class original de las tablas, y tendría que reasignarla ahora. La solución que he empleado me parece más sencilla.

Ver un ejemplo de su funcionamiento.

Descargar el archivo .js.

Personalización

Para emplear el script, basta con vincular por medio de script el archivo .js. Se debe vincular al final del documento, para asegurarse de que todas las tablas se han cargado antes de que comience a ejecutarse.

Aplicar un estilo a las celdas que se resaltan se logra añadiendo una clase .celda_resaltada a la hoja de estilo de la página. El estilo de los encabezados que se muestran debajo se controla por medio de otra llamada .recuerdo_cabecera.

Limitaciones

He comprobado que este script funciona correctamente —sobre Windows— en:

Notas

  1. La detección del soporte de addEventListener la realiza la última línea del script; si el navegador no lo soporta, no se lanza la función escuchar y no se genera error alguno, por ejemplo en Internet Explorer. El método propietario de Microsoft es attachEvent, pero por otras limitaciones que impone la falta de implementación de parte del DOM en este navegador —y que también explico en este artículo—, simplemente no proporciono alternativa para él. Y no me quita el sueño. Volver

Contacto

En virtud de la Ley Orgánica 15/1999 de Protección de Datos de Carácter Personal le informo de que los datos que proporcione no serán empleados para otro fin que el de responder a su mensaje. En especial, me comprometo a no cederlos a terceros ni a emplearlos para enviar información no solicitada.

Del blog de Digital Icon