Y pensar que me lo pasaba pipa con el javascript en mis tiempos mozos y ahora no lo puedo ni ver…
Recientemente me cayo un marron de estos que vienen con efecto: un cliente con algunas nociones de programacion estaba enfrascado en un proyecto titanico (suponia yo) y cuando se atascaba me pedia ayuda, de forma muy puntual y de manera que era imposible determinar a donde queria llegar o que es lo que queria hacer. En varias ocasiones le dije que me contara todo el percal y se lo hacia yo para ganar tiempo pero el, erre que erre seguia en sus trece: al parecer tenia cierta ilusion por hacerlo por si mismo.
Fue pasando el tiempo y finalmente llego el correo fatidico, el correo que sabia que llegaria pero que albergaba la triste esperanza de que, por una vez, el marron no me salpicara. Pero no, todavia tengo el sabor a mierda en la boca.
El cliente queria presentar una serie de datos (familias, subfamilias, productos etc…) en forma de arbol, no queria recargas de pagina de modo que habia que hacerlo todo en javascript en la parte del navegador y de regalo queria un comportamiento no estandard de un TreeView: que al hacer click en una rama se cerrara el resto para no aturullar con muchos datos.: solo podia haber una rama abierta en un momento dado. Tambien queria tener varios nodos raiz, que el arbol se generara dinamicamente en funcion de los articulos que metiera en base de datos etc.
Mirando por la internez vi TreeMenu y algunos otros con muy buena facha, pero claro, el cliente tenia unos gustos tan particulares que modificar los applets que me encontraba era un verdadero cristo, asi que me dispuse a hacerlo por mi cuenta, al menos una base muy simple y orientada a lo que queria este hombre y que luego se pudiera complicar todo lo que se quisiera.
Parece que no, pero presentar datos en formato de arbol cuando el arbol lo tienes que construir por ti mismo es un fregado del copon: nodos padres, hijos, y el espiritu santo; si a eso le añadimos que no soy lo que se dice un programador al 100% (no se si el 15% de mi jornada laboral lo empleo en programar, y si es asi siempre son temas de servidor y bases de datos, algun script suelto por ahi y poco mas) pues al final queda algo que muy posiblemente entre en la categoria de “churro malagueño” pero que basta q me he quemado las pestañas pues lo pongo aqui por si puedo evitar la combustion de pestañas de otros como yo. Hay que tener muy en cuenta un detalle: probablemente la mejor majera de representar un arbol sea precisamente usar un arbol en memoria, buscar el nodo seleccionado por el cliente con algun algoritmo de busqueda eficiente, modificar el atributo de “desplegado” y navegar por los nodos hijos para modificar el estado a “visible”. Yo no lo he hecho asi dado que no tenia mucho tiempo para implementarlo, de modo que la solucion aqui expuesta es muy ineficiente para arboles largos.
Y sin mas dilacion empezemos por la teoria:
Para representar el arbol en un primer momento considere utilizar las listas de HTML (ya sabes “li” y “ul”) pero no quedaban muy bien cuando habia que representar nodos hijos (supongo que con estilos se podria maquear mucho mas pero en el momento en que el html tiene algo que ver con la apariencia y diseño yo desconecto automaticamente) de modo que elegi una estructura tabla para simular el arbol, en la creencia de que este elemento es mas flexible. Un ejemplo de tabla seria este:
| Padre.1. |
|
|
|
|
Hijo.1. |
|
|
|
|
Nieto.1.1. |
|
|
|
|
Biznieto.1.1.1. |
| Padre2 |
|
|
|
|
Hijo.2. |
|
|
Como se ve hay un numero indeterminado de filas que se corresponde con los registros de la base de datos que hayamos recuperado y 4 columnas. Se juega con el atributo “colspan” de las filas para simular la indentacion hacia la derecha para el caso de nodos hijos nietos etc.
Evidentemente la tabla tiene que tener un atributo “border=0″ para ocultar su naturaleza, sino se verian las celdas.
La generacion dinamica de la tabla no la voy a tratar aqui porque varia segun el lenguaje que utilizeis en el servidor pero en lineas generales se trata de lanzar una sentencia sql mas o menos compleja que recupere todos los elementos a mostrar teniendo cuidado de recuperarlo ordenado por las categorias pertinentes. En mi caso tenia que presentar un arbol donde los nodos padre eran “Familias”, dentro de cada familia habia nodos “Subfamilias”, dentro de ellos nodos “Usos”, y finalmente “Productos” de modo que la sentencia SQL tenia que incluir un “ORDER BY Familias, Subfamilias, Usos” para que el arbol estuviera correctamente presentado.
Con el arbol ya generado viene la “magia” del javascript, si bien hay que tener clara las necesidades del script:
Por un lado hay que enganchar de alguna manera la tabla con el javascript, esto es tan facil como añadir un handle para el evento “onmouseclick” de la forma tipica:
"onmouseclick=jTree();".
Con esto cada vez que se haga click en una fila de la tabla se dispara la funcion jTree que aqui estamos presentando al mundo. Ademas, dado que son tablas estaria bien que al pasar el puntero del raton por encima se cambiara a “manita” para saber que es clickable, ergo añadimos
"onmouseover=this.style.cursor='hand'"
que cambia el estilo del cursor a “hand” cuando esta colocado encima de la tabla.
Por otro lado la funcion en javascript tiene que saber sobre qué elemento hemos hecho click para determinar qué rama tiene que desplegar y qué ramas tiene que ocultar. No es tan sencillo como mostrar solo la fila que hemos clickado como se vera a continuacion. Para ello tenemos que nombrar cada fila de forma univoca y no solo eso sino que tenemos que indicar el “camino” del nodo desde el padre hasta llegar a el. Para ello hay q generar un nombre dinamico en funcion de la posicion del nodo en el arbol. Ojo que hablamos de posicion del nodo en el arbol y es lo mismo que decir posicion de la fila en la tabla, ya que hemos quedado que un nodo es en realiad una fila y el arbol es una tabla.
El nombre de cada fila se genera en el servidor (cuando se construye la tabla) y son del tipo “x.y.z.”. Notese que siempre acaba en punto. Asi el nodo “3.” es el 3º nodo raiz, el “2.4.” es el 4º hijo del 2º nodo raiz y asi sucesivamente. Por el numero de puntos que haya en el nombre se sabe el nivel del nodo: 1 punto es nivel 1 osea raiz, 3 puntos son nodos nietos (3º nivel, raiz, hijo y nieto) y asi. Esta informacion va a ser crucial como veremos.
Finalmente, desde un punto de vista algoritmico, la cosa funciona de la siguiente manera:
- El usuario hace click en una fila / nodo.
- El evento dispara la funcion, a la que se le pasa el nombre de la fila.
- La funcion recoge el nombre de la fila y recorre el arbol completo ocultando o mostrando los nodos en funcion de la fila que selecciono el usuario.
Es evidente que cada vez que el usuario haga click se va a procesar todo el arbol y por cada fila se va a decidir que hacer. No se trabaja con subarboles si los cambios afectan a ramas distantes que evitarian tener que recalcular y redibujar todo el arbol entero. Es por tanto una solucion no eficiente apta para arboles de cientos de nodos, pero no plantes un arbol que tenga un millon de nodos porque te vas a chinar por todas las trancas de lo lento que iria. Procesar un millon de entradas puede llevar segundos o quiza minutos.
Decidir que se oculta y que se muestra es un tema que puede resultar complejo aunque una vez entendido no es para tanto. Para coger el hilo tenemos que tener claro los tipos de nodos que nos podemos encontrar. Un nodo es de un tipo u otro siempre tomando como referencia otro nodo: en este caso se compara el nodo que el usuario selecciono con el nodo que estamos examinando en el recorrido completo del arbol. Los tipos de nodos son:
- Raices: los nodos raices son los de nivel 1 y siempre se muestran. Son los unicos nodos que son asi sin necesidad de compararlos con el nodo sobre el que se hizo click.
- Nodos hijos. Los nodos hijos de un nodo son aquellos que dependen directamente de el. Por ejemplo el nodo “1.2.3.4.” es hijo del nodo “1.2.3.”. En este caso es facil ver cuando hay una relacion de padre – hijo: el nombre del padre es el comienzo del nombre del hijo, al que se le añade un nivel mas (en este caso el “4.”). Ademas el nivel (3 el padre) es 1 mayor para el hijo (4 en el ejemplo). Si el usuario hace click en un nodo hay que mostrar todos sus hijos.
- Nodos nietos y mas lejanos: los hijos de los hijos son los nietos. En este caso no se muestran (lo que se hace es una resta de niveles, si es mayor de 1 entonces es un posible nieto, para corroborarlo tendriamos que ver si el nombre del padre pertenece al comienzo del nombre del hijo, cosa q no hace ni falta porque en este tipo de arbol nunca enseñaremos nodos mayores de 1 nivel mas que el nivel del nodo clickado: si clickamos en un nodo de nivel 3 nunca se enseñaran los nodos de nivel 5 en adelante, tengan o no parentesco con el nodo clickado.)
- Nodos hermanos: los que dependen de un mismo padre. Por ejemplo “1.2.3.4.” y “1.2.3.1.” son hermanos ya que ambos tienen a “1.2.3.” como padre. Para comprobar esto simplemente quitamos el ultimo nodo de los nombres y comparamos: “1.2.3.4.” y “1.2.3.1.” se convierten en “1.2.3.”, que es el padre y en ambos coincide, de modo que se determina que son hermanos. Los nodos hermanos y el seleccionado por el usuario (llamado clon en mi nomenclatura particular) siempre se muestran ya que para hacer click en un nodo el usuario tiene que verlo y de cada rama el usuario ve el nodo que hizo click y el resto de hermanos.
- Nodos padres y ancestros: estos nodos junto con los hermanos son los mas complicados de entender: si pulso en “1.2.3.4.” tengo que enseñar el “1.” por ser raiz, el “1.2.” por ser padre, el “1.2.3.” por ser padre… y asi “abrir camino” hasta el nodo que selecciono el usuario. Esto es asi porque en la presente implementacion no se retiene el estado ni las selecciones anteriores como se ha dicho antes: cada seleccion reconstruye el arbol al completo. Pero ojo, que si seleccionamos “1.2.3.4.” tendremos que enseñar “1.2.”… pero tambien “1.2.1.”, “1.2.2.”, 1.2.4.” etc… es decir, tenemos que enseñar los nodos padres… y todos sus hermanos, para de esta forma desplegar el arbol completo hasta llegar al nodo elegido.
Si se tiene claro todo lo dicho hasta ahora el codigo resulta bastante tribial.
function jTree (strNameSelected) {
var i;
var oTabla=document.getElementById("tabla");
var arFilas = oTabla.rows;
var iFila;
if (strNameSelected=='none') {
for (i=0;i<arFilas.length;i++) {
iFila=arFilas[i];
if (iFila.id.match(/\./g).length==1) {
//nodo raiz, se muestra
iFila.style.display="";
}
else {
//el resto se oculta en el arranque de la pagina
iFila.style.display="none";
}
}
}
else {
for (i=0;i<arFilas.length;i++) {
iFila=arFilas[i];
if (DisplayNode(strNameSelected, strNameSelected.match(/\./g).length,iFila.id)) {
iFila.style.display="";
}
else {
iFila.style.display="none";
}
}
}
}
jTree es la funcion principal, es la que recorre todas las filas de la tabla y en funcion de lo que le devuelve la funcion DisplayNode cambia el estilo de la fila. De esta forma:
var oTabla=document.getElementById("tabla");
oTabla almacena el elemento “tabla” de la pagina html (tenemos que tener cuidado de llamar “tabla” a nuestra tabla. Esto evidentemente se puede hacer mas generico).
var arFilas = oTabla.rows;
arFilas contiene un array con una fila de la tabla por cada posicion.
iFila.style.display="";
Dentro del For se recorren todas las filas y se les asigna un estilo, que puede ser “” si se muestra o “none” si se oculta. Si se oculta o no es una decision que se toma en la funcion DisplayNode que se presenta a continuacion. Antes de nada anotar que si la funcion jTree recibe el parametro “none” quiere decir que no se ha hecho click en ningun nodo sino que obedece al arranque de la pagina: ocurre que en un primer momento la tabla se muestra totalmente desplegada tal y como se genera en el servidor y para mostrar solo los nodos raices se hace esta distinticion, de forma que a parte de llamar a la funcion jTree con cada click del usuario en la tabla, tendremos que llamarla en la carga del documento de la forma tradicional:
<body onload="jTree('none');">
Y ahora la madre del cordero:
function DisplayNode (strNameSelected, iLevelSelected,strName) {
var n, iLevel;
if (strName.match(/\./g).length == 1) {
//es nodo raiz, se vera siempre.
return true
}
else {
if (strName.match(/\./g).length - iLevelSelected > 1) {
//nieto o bisnieto etc...
//posible hijo pero esta un nivel de distancia mayor de 1, nunca se mostrara
return false
}
else {
if (strName.match(/\./g).length - iLevelSelected == 0) {
//posible hermano y clon (el mismo)
//se comprueba que vengan del mismo padre, de modo que se corta el ultimo "x."
//se comparan los padres
if (strName.substring(0, strName.substring(0,strName.length-1).lastIndexOf(".")+1) ==
strNameSelected.substring(0, strNameSelected.substring(0,strNameSelected.length-1).lastIndexOf(".")+1)) {
return true
}
else {
return false
}
}
else {
if (strName.match(/\./g).length - iLevelSelected == 1) {
//posible hijo al nivel adecuado.
//se comprueba que el nodo seleccionado esta contenido en el comprobado
if (strName.indexOf(strNameSelected)==0){
return true;
}
else {
return false;
}
}
else {
//el caso mas complejo: padre o hermano del padre
//la resta de niveles es negativa, lo que indica es un nodo anterior
if (strName.match(/\./g).length - iLevelSelected < 0) {
iLevel=strName.match(/\./g).length
//se mira unicamente si es su padre. No hace falta mirar todos los tramos
//dado que esas comprobaciones ya se han hecho antes.
if (strNameSelected.indexOf(strName.substring(0, strName.substring(0,strName.length-1).lastIndexOf(".")+1))==0) {
return true;
}
else {
return false;
}
}
else {
alert ("pepee que te pasas");
}
}
}
}
}
}
Los 3 parametros que recibe la funcion son:
- strNameSelected: nombre del nodo seleccionado por el usuario.
- iLevelSelected: nivel del nodo seleccionado por el usuario.
- strName: nombre del nodo inspeccionado por el bucle For.
En esta funcion se compara strNameSelected con strName y se devuelve true o false segun se tenga q mostrar o no. La funcion se compone de varios ifs anidados para tratar de determinar el tipo de nodo que estamos tratando. Aunque se ha usado la intuicion, lo suyo es ordenar la clasificacion de mayor a menor probabilidad: asi por ejemplo en arboles pequeños hay una alta probabilidad de que el nodo que estemos tratando sea raiz, en arboles muy grandes las probabilidades de que sea nieto seran mayores. Es por ello que para ahorrarnos comprobaciones hay que anidar los Ifs de forma un poco logica. Se podria haber usado un case para ganar legibilidad pero he pasado por pura vagancia: no tenia ganas de mirar la sintaxis del case (o switch o como quiera que se llame en este lenguaje).
if (strName.match(/\./g).length == 1)
En este tipo de sentencia se mira el numero de “.” que tiene el nombre, si es 1 es un nodo raiz, asi que se muestra.
En otros casos se compara con el nivel del nodo seleccionado realizando una resta de niveles: si la resta es 0 es posible q sean hermanos, si es 1 es posible que sean hijos, si es negativo es posible que sean padres. Si es mayor de 1 entonces es posible que sean nietos.
if (strName.substring(0, strName.substring(0,strName.length-1).lastIndexOf(“.”)+1) == strNameSelected.substring(0, strNameSelected.substring(0,strNameSelected.length-1).lastIndexOf(“.”)+1)) {
En este churro de linea lo que se hace es buscar la posicion del 2º punto desde la derecha del nombre, quitar a partir de ahi y comparar el substring resultante para ver si dos nodos tienen el mismo padre. Si tienen el mismo padre tienen que ser exactamente iguales ambos substrings.
Y eso es todo, el asunto no da para mas. Y feliz año nuevo!