Menu horizontal basado en el de Roger Johansson

Nace una idea

Hace algunas semanas, por la petición de un compañero de trabajo, me surgió la necesidad de hacer una menú horizontal con doble nivel, es decir, un menú horizontal con un submenú para cada uno de sus elementos. Sé que hay muchos modos de solventar el problema pero me acordé de la propuesta de Roger Johansson, que mi amigo Marco tiene colocada en su Web, y me piqué con la idea de adaptarlo para este caso.

La idea que me surgió para el menú fué la de uno que se desplegase dentro de dos áreas horizontales contigüas verticalmente y de altura fija, o lo que es lo mismo, dos bandas de color de la misma altura y una encima de otra, tal y como se muestra en el siguiente esquema.

---------------------------------------------------------------------------------------------
| Zona I: zona de visualización de los elementos del menú superior                           |
---------------------------------------------------------------------------------------------
| Zona II: zona de visualización del submenú de cada uno de los elementos del nivel superior.|
---------------------------------------------------------------------------------------------

O como se ve en la siguiente imagen

Menu horizontal

Analizando la propuesta de Johansson

Roger Johansson utiliza únicamente tres elementos para crear su menú: un código HTML correcto, una hoja de estilos y un fichero JavaScript sencillo de entender y muy efectivo (no pocos lo hubieran hecho con jQuery o Mootools pero en ellos queda hacer la pertinente versión). Los submenús, de un elemento del menú padre, son desplegados y replegados mediante los consiguientes clicks del ratón: uno para desplegar y otro para replegar. Es decir, es necesaria la interacción del usuario para replegar los submenús ya desplegados.

Este comportamiento, que para muchas situaciones es válido, en nuestro caso (el doble menú horizontal en dos bandas, o zonas), no lo es. Necesitamos que estando desplegado un submenú, ocupando toda la Zona II, seleccionemos otro elemento del menú padre, de la Zona I, aquel se repliegue y se despliegue el submenú del elemento seleccionado. Buff! no sé si me estoy explicando u os estoy liando. Vayamos más despacio: si tenemos desplegado el submenú del elemento_1, llamado submenu_1, y queremos ver el submenú del elemento_2, llamado submenu_2 tendremos que:

  1. pinchar en elemento_2
  2. replegar submenu_1
  3. desplegar submenu_2

Es decir: antes de desplegar un nuevo submenú hemos de replegar el que esté desplegado o, lo que es lo mismo, antes de desplegar un nuevo submenú hemos de replegarlos todo.

El código JavaScript

Manos a la obra

Llevando esta idea al código JavaScript, lo que vamos a hacer es modificar el método toggle(), de la clase toggleMenuH() (llamada así para diferenciarla de la original toggleMenu()), para que sea capaz de replegar todos los submenús.

Código original:

toggle : function(el, sHiddenClass) {
  var oRegExp = new RegExp("(^|\\s)" + sHiddenClass + "(\\s|$)");
  el.className = (oRegExp.test(el.className)) ? el.className.replace(oRegExp, '') : el.className + ' ' + sHiddenClass;
}

Código modificado:

toggle : function(el, sHiddenClass, bHide) {
  if(bHide == true) this.hideAll(el, sHiddenClass)
  var oRegExp = new RegExp("(^|\\s)" + sHiddenClass + "(\\s|$)");
  el.className = (oRegExp.test(el.className)) ? el.className.replace(oRegExp, '') : el.className + ' ' + sHiddenClass;
}

Lo único que hemos hecho es incluir una variable para saber si hemos de ocultar todos los elementos del menú (la presencia de esta nueva variable obligará a modificar todas las llamadas que se hagan a esta función para tenerla en cuenta), y crear la función, llamada hideAll(), que se encargará de ocultar todos los submenús cuando sea necesario.

hideAll : function(el, sHiddenClass){
  for(i = 0; i < arrMenus.length; i++){
    arrSubMenus = arrMenus[i].getElementsByTagName('ul');
    for (var j = 0; j < arrSubMenus.length; j++) {
      oSubMenu = arrSubMenus[j];
      if(el !== oSubMenu){
        var oRegExp = new RegExp("(^|\\s)" + sHiddenClass + "(\\s|$)");
        if(!oRegExp.test(oSubMenu.className)) {oSubMenu.className = oSubMenu.className + ' ' + sHiddenClass;}
      }
    }
  }
}
Vamos un poco más allá

En cuanto le envié el nuevo menú a mi gurú del CSS, el Sr. Marco Giacomuzzi, me retó a darle una vuelta de tuerca más y dotarle de alguna nueva funcionalidad. De las que comentaba las más destacables fueron:

  • Encerrar cada uno de los enlaces dentro de una etiqueta HTML, lo que permitiría un mejor control sobre el elemento a la hora de maquetar el menú.
  • Resaltar la opción del menú superior asociada al submenú desplegado.

La implementación de estas dos nuevas cualidades provocaron una serie de cambios en el código JavaScript, que a continuación veremos. La introducción de un elemento HTML para que contenga a los enlaces provoca una cambio en la ruta de acceso, vía DOM, desde el nodo del enlace hasta el elemento de la lista que lo contiene. Es decir, que en la función toggleMenuH.ini() pasamos de tener:

oLink.onclick = function(){
  toggleMenuH.toggle(this.parentNode.getElementsByTagName('ul')[0], sHiddenClass, sTag, sOverParent, true);
  return false;
}

a tener esto otro (fijaros en la palabra en negrita).

oLink.onclick = function(){
  toggleMenuH.toggle(this.parentNode.parentNode.getElementsByTagName('ul')[0], sHiddenClass, sTag, sOverParent, true);
  return false;
}

Sencillo, ¿no? Veamos ahora qué necesitamos para poder resaltar la opción de menú asociada al submenú desplegado. Intuitivamente vemos que lo que necesitamos es cambiar el estilo del elemento del menú del primer nivel cada vez pinchemos sobre él. O lo que es lo mismo, necesitamos cambiar el estilo del elemento del menú del primer nivel cada vez que que despleguemos o repleguemos su submenú asociado. Es decir, que hemos de modificar el método, o función, que ejecuta esta última idea, que no es otro que el método toggleMenuH.toggle(), para localizar el elemento padre y aplicarle, o quitarle, el estilo que se encargue del resalte (estilo que debemos pasar a la clase toggleMenuH y que debe ser incorporado a todo los métodos que hagan uso de él). Al implementarlo esta idea en el código se pasa de tener:

toggle : function(el, sHiddenClass, bHide) {
  if(bHide == true) this.hideAll(el, sHiddenClass)
  var oRegExp = new RegExp("(^|\\s)" + sHiddenClass + "(\\s|$)");
  el.className = (oRegExp.test(el.className)) ? el.className.replace(oRegExp, '') : el.className + ' ' + sHiddenClass;
 }

A tener:

toggle : function(el, sHiddenClass, sTag, sOverParent, bHide) {
  if(bHide == true) this.hideAll(el, sHiddenClass, sOverParent)
  var oRegExp = new RegExp("(^|\\s)" + sHiddenClass + "(\\s|$)");
  var oRegExpParent = new RegExp("(^|\\s)" + sOverParent + "(\\s|$)");
  eval("t = el.parentNode.getElementsByTagName('"+sTag+"')[0].getElementsByTagName('a')[0]");
  if(oRegExp.test(el.className)){
    el.className = el.className.replace(oRegExp, '');
    t.className = t.className + ' ' + sOverParent;
  }
  else{
    el.className = el.className + ' ' + sHiddenClass;
    t.className = t.className.replace(oRegExpParent, '');
  }
}

Fijaros en las líneas en negrita, son las encargadas de hacer lo que antes comentábamos sobre identificar al padre y modificarlo con un nuevo estilo.

El código CSS

El modo de presentar los elementos en la página depende ámpliamente de la persona que lo haga; en nuestro caso la hoja de estilos intenta ceñirse a nuestro planteamiento inicial (un menú horizontal de dos niveles con los elementos del primer nivel en la Zona I y los del segundo nivel en la Zona II) así que veréis que la hoja de estilos está muy adaptada a las necesidades del problema y que no es fácilmente utilizable en otras situaciones.

Ya casi acabamos

Como no podía ser menos, en cuanto aplicamos la hoja de estilos y probamos todo junto nos damos cuenta que nuestro amigo Internet Explorer 6 tenía problemas para interpretar nuestro código y no ajusta correctamente el tamaño del submenú desplegado (en lugar de ajustarlo al 100%, como fija la clase second_menu de la hoja de estilos, .lo ajustaba al ancho del mayor de los textos incluidos en alguno de los elementos del submenú). A la vista del problema, y ya que vía CSS no pude solventarlo, decidí usar JavaScript para hacerlo.

La idea de lo que se tiene que hacer era clara: hay que que asignar al submenú la anchura del menú principal. Para ello creamos un nuevo método para la clase toggleMenuH llamado equalWidth que realiza lo establecido: busca el primero de los elementos, del tipo dado por la variable oElm, que tengan la clase strClassFather y establece su anchura de diseño, dada por su propiedad offsetWidth, como la anchura de su submenú asociado.

equalWidth : function (oElm, strClassFather, strClassChild){
  arrMenus = this.getElementsByClassName(document, oElm, strClassFather);
  if(arrMenus[0]){
    var dWidth = parseInt(arrMenus[0].offsetWidth);
    arrSubMenus = this.getElementsByClassName(document, oElm, strClassChild);
    for(var j=0; j<arrSubMenus.length;j++){
      oSubMenu = arrSubMenus[j];
      oSubMenu.style.width = dWidth + 'px';
    }
  }
}

Para que esta corrección de dimensiones tenga en cuenta todas las posibilidades de actuación del usuario debe ser incluida en los eventos onload y resize de la página.

Resultado final

Las modificaciones que han sido incorporadas provocan que sean necesarios más datos a la hora de iniciar el proceso (recordad que se hace con la última línea del documento JavaScript hmenu.js). El significado de las siguientes líneas de código es establecer que en el momento de finalización de la carga de la página se pongan en marcha las funciones de preparación del menú y de corrección de las anchuras (la misma que se pone en marcha también en el momento de redimensionar la página).

toggleMenuH.addEvent(window, 'load', function(){
  toggleMenuH.init('first_menu', 'hidden', 'span', 'select');
  toggleMenuH.equalWidth('ul','first_menu', 'second_menu');
})
toggleMenuH.addEvent(window, 'resize', function(){
  toggleMenuH.equalWidth('ul','first_menu', 'second_menu');
})

Los argumentos que se le deben pasar a la función toggleMenuH.init, tal y como vemos en el ejemplo, son:

  • first_menu: el nombre de la clase que se aplicará a la lista, etiqueta <ul>, de elementos del primer nivel del menú.
  • hidden: el nombre de la clase que se aplicará a cada uno de los submenús, o listas del segundo nivel, para ocultarlos.
  • span: la etiqueta HTML que será usada para encerrar cada uno de los enlaces del menú y dar así mayor versatilidad a la hora de maquetar el menú.
  • select: el nombre de la clase que se aplicará al elemento del primer nivel que tenga su submenú desplegado, es decir, el elemento activo.

Así mismo, los argumentos para la función toggleMenuH.equalWidth, tal y como vemos en el ejemplo, son:

  • ul: el elemento HTML sobre el que buscaremos aquellos que tengan las clases dadas por los demás parámetros.
  • first_menu: el nombre de la clase que se ha establecido en el elemento HTML, <ul>, del menú principal.
  • second_menu: el nombre de la clase que se ha establecido en el elemento HTML, <ul>, del menú secundario.

No sé si la explicación ha quedado clara o no, así que mejor será que os proporcione una demostración de lo que os he estado contando. Los ficheros que necesitas para reproducir el ejemplo son el código JavaScript y el código CSS

A petición popular

Como varios de vosotros me habéis preguntado por el modo de conseguir que al cargar la página una de las opciones del menú superior ya esté desplegada, he modificado el código JavaScript del menú para que lo tenga en cuenta. Lo único que he hecho es incluir un parámetro más en toggleMenuH.init() para indicar qué elemento del menú superior queremos desplegar. Así, pasamos de tener

init : function(sContainerClass, sHiddenClass, sTag, sOverParent)

a tener

init : function(sContainerClass, sHiddenClass, sTag, sOverParent, iViewIndex)

donde, si consideramos las opciones del menú superior como los elementos de un array, iviewIndex indica el elemento del array de opciones de menú superior que queremos desplegar.

Si ahora volvéis al ejemplo del doble menú horizontal veréis que automáticamente se muestra la opción 2 (Seires de televisión), teniendo en cuenta que los indices de los arrays en Javascript comienzan en 0

Avisos y consideraciones

No podemos finalizar el texto sin poner de manifiesto que tanto la programación en JavaScript como la hoja de estilos están adaptadas para un menú horizontal de doble nivel. En el caso de menús de mayor profundidad u horientación sería necesario readaptarlo todo a las nuevas situaciones. Este menú nunca pretendió ser la respuesta a todos los menús horizontales, esa es otra batalla, pero esperamos que a alguien le pueda ser útil. En lo referente a la compatibilidad con navegadores, el menú responde a las espectativas de correcta visualización con IE6, IE7, Firefox 2.0.* y Opera 9.* (en WINDOWS XP Prof. SP2).