Como convertir un puñado de listas anidadas en un menú vertical multinivel con la ayuda de JavaScript de una manera no obstrusiba.
El problema
Para los que venimos del mundo del JavaScript de los navegadores 4.0 (antes conocido como DHTML) , construir un menu multinivel para nuestra página web con DHTML, es algo “normal” o cuanto menos facil de implementar con los cientos de librerias que lo implementan, pero en el mundo de la WEB 2.0 donde el uso del JavaScript ha de ser inobstrusivo (esto es, que la pagina funcione sin él) y la accesibilidad es una obligación, el uso tradicional del que venia hablando pues es cuanto menos incorrecto: el menú (y las páginas a las que apuntan sus items) simplemente no aparecen si el usuario lo tiene desactivado o su navegador no lo soporta.
Asi pues, se plantea la necesidad de encontrar otra manera de conseguir el mismo objetivo adaptándose a los entándares actuales a la vez que diseñamos nuestra web de una manera más lógica y separamos la estructura, la presentacion y el código para que lo más importante, el contenido de nuestra página pueda ser accedido sin dificultad.
Mi solución
Llegados a este punto, es de remarcar que la solución que planteo no es original en el sentido que otros han pensado lo mismo y han llegado a soluciones parecidas, como por ejemplo, menús multinivel sólo con CSS, pero yo buscaba algo más tradicional, del tipo “hasta que no hago click no se desplega el menu” y así acabe implementando este trozo de codigo.
Las características que definen la implementación son estas:
- El menú estará definido en XHTML por unas serie de listas anidadas
- Todas las opciones del menu han de poder ser visibles si JavaScript esta desactivado
- El código ha de ser ligero y compatible con el DOM de los navegadores más importantes
- Su uso ha de ser simple
Cómo usar el código
Antes de explicar su funcionamiento (podeis verlo en marcha en esta página), explicaré como incluirlo en vuestras páginas, simplemente se ha de incluir el fichero JavaScript:
Y después añadirlo al evento window.onLoad():
else window.addEventListener("load", DHmenuvm, true);
Opcionalmente, si se quiere aplicar el código a una lista con un identificador diferente a “DHmenuvm”:
DHliMenu('ID del elemento');
}
if (window.attachEvent) window.attachEvent('onload', DHmenuvm_onLoad);
else window.addEventListener("load", DHmenuvm_onLoad, true);
El código para añadir el evento onload() es muy simple pero suficiente para un par de ejemplos, aunque siempre es conveniente algo más elaborado para un sitio en producción.
Para empezar
Lo primero que necesitamos es el markup XHTML con el que vamos a trabajar (por cierto, no recuerdo de dónde copie la lista). Para mantener los ejemplos de una manera mas compacta, el JavaScript, XHTML y CSS van juntos en un único fichero ¡Aunque recordad siempre que se han de mantener por separado! Como el ejemplo es un poco extenso, prefiero que consulteis el código fuente del ejemplo en vez de publicarlo para hacer la explicación mas sencilla de entender.
Como se puede ver, el código del que partimos son simplemente unas listas anidadas a las que hemos añadido un poco de estilo. El unico detalle remarcable es que hemos añadido un clase parent a las listas que tienen sublistas para dar una sensación visual de que hay más opciones. Tambien es remarcable que esto será lo que veran aquellos navegadores que no tengan habilitado JavaScript, lo que tampoco es tan malo si pensamos que ya habiamos destinado la parte izquierda para un menu.
Lo siguiente es ocultar todas las UL’s que están por debajo del primer nivel, lo cual a priori es facil, pero en la práctica se complica un poco, ya que hemos de buscar una regla concreta del CSS:
display:inline;
margin:0;
}
y cambiar display:inline a display:none:
var dss = document.styleSheets[0]
var ss0 = dss.cssRules || dss.rules
for (var x in ss0)
if ((ss0[x].selectorText + '').toLowerCase() == '#' + tagId.toLowerCase() + ' li ul')
ss0[x].style.display = 'none'
Para ello identificamos el id de la lista principal (variable tagId). Este id es por defecto “DHmenuvm”, pero le podemos pasar a la función como primer argumento el que más nos interese, pero precisamente el primer argumento es el objeto event en caso de usar la función como argumento de window.onload:
else window.addEventListener("load", DHmenuvm, true);
Por lo que hemos de comprobar primero que es una cadena, de ahí la complicación. Después recorremos todos los selectores de la primera hoja de estilo (y si, seria mejor recorrer todas las hojas) buscando el selector que antes mencionamos para poner el display:none.
El meollo
Bueno, ya hemos ocultado las UL que están por debajo de la principal, ahora nos toca mostrarlas cuando hacemos click en los LI. Como se puede deducir, lo mas obvio es localizar todos los LI del elemento que vamos a convertir en un menú (en nuestro ejemplo “DHmenuvm”) y asignarles un evento onclick, pues dicho y hecho:
if (li) {
li = li.getElementsByTagName('LI');
var prev_pc = 0;
var prev_li = null;
for (var i in li) {
li[i].onclick = function(e) {
// un montón de codigo por explicar todavía
}
}
}
El código se explica por si mismo (las variables de por medio nos servirán más adelante), asi que iremos por lo que de verdad importa, el código de la función onclick(), y lo primero que vamos a hacer es contar los UL’s que el LI actual tiene por encima:
var t = this;
while(t = t.parentNode) if (t.tagName == 'UL') pc++;
¿Y para que queremos saberlo? Pues para ocultar los items que estan visibles si clickeamos un item por debajo del ultimo mostrado; en nuestro menú de ejemplo, si desplegamos “Main Courses” y luego en “Burger Meals”, hemos de ocultar toda esa rama si clickeamos en “Drinks”. Y eso es lo que hace el código de abajo, claro que implementarlo es más dificil que explicarlo… Para entenderlo, primero hemos de definir la función dc():
for (var c = l.firstChild; c = c.nextSibling;)
if (c.tagName == 'UL') c.style.display = s? 'inline' : 'none';
}
Su funcionamiento es muy simple: le pasamos un elemento y cambia el display de los elementos UL en el mismo nivel que ese elemento en función del segundo argumento de la función. Con función, ahora es más facil ocultar los elementos. El resto de código sigue así:
dc(prev_li, false);
for (var c = prev_li, d = prev_pc - pc; d > 0 ;c = c.parentNode)
if (c.tagName == 'UL') { c.style.display = 'none'; d-- }
}
Es decir, si el LI sobre el que hacemos click esta en el mismo nivel o por debajo del anterior que se pulsó, oculta todos los items en su mismo nivel y los que tiene por encima. Y echo esto, ahora ya solo nos queda mostrar los UL que hay en el mismo nivel y actualizar los valores de las variables de estado:
prev_pc = pc;
prev_li = this;
Por cierto, los lectores avispados ya se habran dado cuenta que al actualizar dichas variables estamos creando una closure pero tranquilos, estan funciones no tienen “pérdidas“.
Ya sólo nos queda un pequeño retoque para acabar nuestra función, que es cancelar el “bubling” del evento que estamos procesando:
Y ahora ya podemos mostrar completo todo el código
El código completo
function dc(l, s) {
for (var c = l.firstChild; c = c.nextSibling;)
if (c.tagName == 'UL') c.style.display = s? 'inline' : 'none';
}
tagId = (tagId && (typeof(tagId) == 'string')) || 'DHmenuvm'
var dss = document.styleSheets[0]
var ss0 = dss.cssRules || dss.rules
for (var x in ss0)
if ((ss0[x].selectorText + '').toLowerCase() == '#' + tagId.toLowerCase() + ' li ul')
ss0[x].style.display = 'none'
var li = document.getElementById(tagId)
if (li) {
li = li.getElementsByTagName('LI');
var prev_pc = 0;
var prev_li = null;
for (var i in li) {
li[i].onclick = function(e) {
var pc = 0;
var t = this;
while(t = t.parentNode) if (t.tagName == 'UL') pc++;
if (pc <= prev_pc) {
dc(prev_li, false);
for (var c = prev_li, d = prev_pc - pc; d > 0 ;c = c.parentNode)
if (c.tagName == 'UL') { c.style.display = 'none'; d-- }
}
dc(this, true);
prev_pc = pc;
prev_li = this;
if (e) e.cancelBubble = true; else event.cancelBubble = true;
}
}
}
}
Descargas: menu-final.html | menu-comentado.html
Licencia
Este código es libre de usarse y modificarse en cualquier proyecto personal o comercial siempre que se mantenga un comentario en el mismo que contenga un link a esta página y mi autoría, concretamente algo como esto:
// http://www.ismaelj.com/articulos/menu-vertical-multinivel/
Felicitarte por darnos la oportunidad de avanzar en nuestros trabajos con páginas como la tuya. Realizar un menú con estilos es lo que iba buscando y quería darte las gracias por mostrarlo sin pedir nada a cambio.
Gracias por compartir tus conocimientos con todos, asi avanzamos y progresamos para hacer que este mundo sea cada vez mejor.
Es genial que compartas tu fuente, de verdad muchas gracias realmente es de mu cha utilidad para mi trabajoo…..
Gracias, estoy empezando y necesita un codigo como este.
Sencillo pero en verdad util amigo gracias
Se podria implemetar algo para que se abra cierta seccion del menu dependiendo una variable mandada ?
Amigo muchas gracias por tu aporte, peo tengo un problema descargo el archivo que me das, pero cuando abro la pagina web, tengo todo los sub menus abiertos. como hago parta que me aparezcan cerrados apenas abro la pagina. gracias
Amigo muchas gracias, aún en estos tiempos tan avanzados, algo que nunca cambiará es que todos necesitamos de todos, bendiciones, hasta pronto
¡Hola gracias por el código!
Me podrías decir como podría hacer para que cuando cargue la pagina se abra un submenu en concreto?
thank you very much, for you help, my personal opinion is
that you code is very very good, by jose luis lozano
Gracias por compartirlo, me ha resultado muy útil para aprender algunas cosas que no encontraba por ninguna parte. Gente como tú es la que hace que otros desarrolladores publiquemos nuestros trabajos, mil gracias.