tierra de nómadas - recortes

Convertir una lista en un menú desplegable

Desplazarse al índice de navegación.

La idea es convertir una lista cualquiera, de esas que tienen listas anidadas, en una lista desplegable. Dicho de otra forma, idear un mecanismo para que, al pulsar sobre un elemento, se muestren u oculten sus hijos. Adicionalmente, para alegrar un poco este ensayo, sustituiremos las viñetas de lista por pequeñas imágenes con los símbolos '+' (más) y '–' (menos). Partimos de la condición de no tocar el código HTML salvo para identificar la lista. De esta forma, cualquier actualización, inclusión de nuevas sublistas, etc., no supondrá ningún problema.

En una primera función, que se ejecutará en el onload del body, se hace desaparecer cualquier sublista, se aplica la imagen '+' a todos los elementos, y se agrega el perceptor del evento click a todos los elementos con listas hijas. Por ejemplo:

function Iniciar(menuId) {
var Menu = document.getElementById(menuId);
var Submenues = Menu.getElementsByTagName('ul');
for (var i=0; i < Submenues.length; i++) {
  Submenues[i].style.display='none';
  Submenues[i].parentNode.style.listStyleImage = 'url(block.png)';
  Submenues[i].parentNode.addEventListener('click', Accionar, 0);
  }
}

La función que oculta o muestra las listas sólo debe chequear la lista hija, y aplicarle el estilo CSS "contrario" (jugamos con los valores block y none de la propiedad display. También cambiaremos la imagen '+' a '–', o viceversa. Por otro lado, debemos tener cuidado con el "burbujeo" del evento, porque puede disparar el proceso en el elemento padre del actual (y así sucesivamente), situación que sería caótica. Para evitarlo, cancelamos la función si el elemento que ha recibido el evento no es un li, y paramos la propagación tras ejecutar el cambio:

function Accionar(E) {
var elmLI = E.currentTarget;
if (elmLI.nodeName!='LI') return null;
var estiloActual;
for (var i=0; i < elmLI.childNodes.length; i++) {
  if (elmLI.childNodes[i].nodeName=='UL') {
    estiloActual = elmLI.childNodes[i].style.display;
    elmLI.childNodes[i].style.display = estiloActual=='none') ? 'block' : 'none';
    elmLI.style.listStyleImage = 'url('+estiloActual+'.png)';
    }
  }
E.stopPropagation();
}

Observese que los archivos correspondientes a las imagenes '+' y '–' se llaman block.png y none.png respectivamente, para poder referirnos a ellos directamente a partir de la propiedad correspondiente.

Lo anterior será suficiente para cualquier aplicación que implemente el módulo Eventos del segundo nivel del DOM. Pero en el momento de redactar esto, sólo los navegadores basados en Gecko cumplen esta premisa. De manera que podríamos tratar de escribir un código alternativo para navegadores que sólo implementen el nivel 1 del DOM. Para ello utilizaremos el modelo de eventos de JavaScript. En este caso la propagación es un problema, lo que finalmente me ha obligado a recurrir a una variable global que me indique el último elemento que disparó el evento, con el fin de superar ciertos conflictos. Para rematar la chapuza, se ha cambiado listStyleImage por backgroundImage para evitar fallos en algunas versiones, y se han agregado un par de líneas para precargar la imagen '–'. El script completo es:

if (document.implementation) {
  if (document.implementation.hasFeature('Events','2.0')) var DOM2 = true;
  else {
    var DOM1 = true;
    var elmObj = null;
    }
  }
else {
  if (document.getElementById) {
    var DOM1 = true;
    var elmObj = null;
    }
  }
if (DOM1 || DOM2) {
  var Imagen = new Image;
  Imagen.src = 'estilos/rc009/none.png';
  }
 
function Iniciar(menuId) {
if (!DOM1 && !DOM2) return null;
var Menu = document.getElementById(menuId);
var Submenues = Menu.getElementsByTagName('ul');
for (var i=0; i < Submenues.length; i++) {
  Submenues[i].style.display='none';
  Submenues[i].parentNode.style.backgroundImage = 'url(block.png)';
  if (DOM1) Submenues[i].parentNode['onclick']=new Function('Accionar(this);');
  if (DOM2) Submenues[i].parentNode.addEventListener('click', Accionar, 0);
  }
}
 
function Accionar(E) {
var elmLI = (DOM1) ? E : E.currentTarget;
if (DOM1) {
  if (elmObj == null) elmObj = elmLI;
  if (elmObj.parentNode.parentNode == elmLI) return elmObj = elmLI;
  }
if (DOM2) {
  if (elmLI.nodeName!='LI') return null;
  }
var estiloActual;
for (var i=0; i < elmLI.childNodes.length; i++) {
  if (elmLI.childNodes[i].nodeName=='UL') {
    estiloActual = elmLI.childNodes[i].style.display;
    elmLI.childNodes[i].style.display = (estiloActual=='none') ? 'block' : 'none';
    elmLI.style.backgroundImage = 'url('+estiloActual+'.png)';
    }
  }
if (DOM1) elmObj = elmLI;
if (DOM2) E.stopPropagation();
}

A continuación una aplicación del sistema. Hay que tener en cuenta que se conseguirían resultados más óptimos y exactos si los detonantes de los eventos fuesen elementos en línea (a o span, por ejemplo) en lugar de los elementos li. La razón resulta evidente cuando se juguetea un poco con la lista, pero he preferido dejarla así para cumplir con la premisa de no tocar la lista original.



Sugerir cualquier cosa, contactar, etc...

Avanzada...

15/11/2002. sysifus. Recorte nº 9.

Estás en: tierra de nómadas > recortes > Convertir una lista en un menú desplegable.