menu horizontal

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).

31 thoughts on “Menu horizontal basado en el de Roger Johansson”

  1. Gracias por tu menú.
    Si tienes tiempo quería consultar:
    Se puede hacer que aparezca una opción seleccionada y que muestre su submenú? al entrar en la web.
    Gracias, Jose.

  2. Hola Jose,

    buena idea la que propones. La he estado mirando y es sencillo hacerlo (sólo hay que modificar levemente el JS del menú). Ahora es un poco tarde para publicar una artículo con el nuevo código pero en breve (mañana o pasado) publicaré un artículo con las modificaciones necesarias. Así todo, si tienes prisa ponte en contacto conmigo por email y te cuento, Ciao 🙂

  3. Buenas,

    he implementado el menu en una web, pero resulta que no todas las opciones del menu principal tienes subopciones. El problema es que cuando la opcion principal no tiene subordinadas, no funciona el link. Muchas graciaso

  4. Hola max,

    Siguiendo con el el ejemplo si quisieramos hacer que el elemento ‘Inicio‘ fuese un enlace a Google, en lugar de ser un enlace sin destino, únicamente tendríamos que sustuir la línea de código donde pone:

    <li><span><a href=“javascript:void(0)”>Inicio</a></span></li>

    Por esta otra:

    <li><span><a href=“http://www.google.es/”>Inicio</a></span></li>

    Con esto, debiera funcionar correctamente el enlace a Google.es

  5. Hola Jorge, muchas gracias por la respuesta, he probado y no me funciona, la @ me da error.
    La cosa es que no me funciona cuando en una opcion del first menu en vez de tener subopciones, quiero meter un enlace. Trasladado a tu ejemplo es como si quisiera que en la opcion Automoviles, al hacer clic, en vez de aparecer Lancia, Subaru, etc… quisiera meter directamente un link a un pagina (porque automoviles no tenga subopciones por ejemplo). Parece como si en javascript capturase el clic y lo ignorara. Gracias y un saludo.

  6. Max,

    perdona pero ha sido culpa mía, la @ sobraba (se me coló al intentar meter código HTML en el comentario). He modificado el ejemplo para que haga lo que entiendo que deseas, dime si es así 🙂

  7. Hola Jorge,
    Estoy basándome en tu excelente menú para una web que tengo que realizar. Mi pregunta es la misma que hacía el otro Jose. Como hacer para que al cargar la web aparezca marcada una opción por defecto. Por ejemplo la opcion Inicio que mantiene oculto todos los submenús hasta que el visitante empiece a navegar…

    Gracias.

  8. Hola Jose,

    acabo de modificar el artículo para explicar lo que pides, en el ejemplo ya lo hice hace unos días pero me quedé sólo en eso 🙂 Espero que te sea de ayuda, ciao

  9. Gracias Jorge, solucionado, como bien dices ya lo habías dejado hecho, pero no me había dado cuenta de que estaba a 2 por defecto.

    Otra pregunta, y disculpa mi ignorancia del lenguaje. Cuando navegas por la web con ese menu implementado y por ejemplo pinchas sobre uno de los enlaces del segundo nivel, este te llevará a una sección de la web. Hasta ahí todo ok, pero al pinchar sobre el link y cargar dicha sección, el menú conserva el desplegado correspondiente o hay que indicarlo de algún modo?. Menudo lio que he montado.

    Gracias.

  10. Hola Jose,

    en el caso que dices no se mostraría automáticamente, habría que indicarlo. Ten en cuenta que al menú debemos decirle qué sección debe desplegar en cada momento, bien sea mediante el click del ratón o en la carga del mismo.

    Pensando en la manera de hacer lo que dices si la página se genera mediante algún lenguaje de servidor (PHP, ASP, JSP, etc) no sería demasiado complicado hacer que sea este quien genere las líneas que ponen en marcha el menú indicándole ya el menú a desplegar. En el caso de utilizar únicamente HTML tendríamos que, en la carga del documento, indicar de algún modo que JavaScript pueda entender la sección de quebe desplegar y, para hacerlo elegantemente, debiera ser el propio script del menú quien modificase los enlaces para que ya tuviera esto en cuenta. Si tengo tiempo intentaré ver qué se puede hacer 😉

  11. Hola Jose,
    Fantastico tutorial,es justo lo que andaba buscando,pero tengo un problema,si yo asigno un enlace a los elementos de primer nivel no funcionan,es obligatorio asignar el enlace al segundo nivel?o hay alguna forma que los enlaces funcionen también en el primer nivel?

  12. el script es el que andaba buscando pero sigue teniendo el mismo problema..el menú principal tiene que enlazar con la página destino aparte de desplegarse el sub.m al cargar.. que tengo que cambiar en el JS??

  13. Hola Juan,

    no se si entiendo lo que dices: ¿me preguntas si es posible que los enlaces del menú, del primer nivel, sean al mismo tiempo enlaces a direcciones externas y que desplieguen su submenú asociado?
    Si esto es lo que planteas te pregunto yo: ¿tiene sentido, para el usuario, que un mismo enlace haga dos cosas? ¿que el enlace que le despliegua el submenú ala vez le mande a una URL? ¿Qué acción gana? ¿cuál debe ser el resultado final de esta acción?

    Si me aclaras cuál es el efecto deseado podría decirte algo más 🙁

  14. A lo mejor no me he explicado bien. tengo una web donde tiene varias páginas..y quiero usar este menú.. el primer nivel me debería de llevar/cargar al hacer click en la cabecera de cualquier opcion de menu a la página está claro..y evidentemente una vez cargada la página que se active el submenu de esa opción..esto es lo normal y como está ahora no lo hace.. ahora solo es un primer nivel estático que por js despliega las opciones del segundo nivel.. la verdad que sentido tiene? de hecho el post anterior al mio hace la misma consulta..espero haberme explicado ahora..un saludo. Juan.

  15. Hola Juan Manuel,

    con el ejemplo creo que entendí lo que necesitas: poder indicar en cada carga de la página qué opción de menú desplegar. ¿Esto eso? En este caso, se podría solventar si modificas el final del fichero hmenu.js de modo que donde pone:

    /* Beginning of the process */
    toggleMenuH.addEvent(window, 'load', function(){
      toggleMenuH.init('first_menu', 'hidden', 'span', 'select', 2);
      toggleMenuH.equalWidth('ul','first_menu', 'second_menu');
      })
    toggleMenuH.addEvent ( 
    window, 'resize', function(){ toggleMenuH.equalWidth('ul','first_menu', 'second_menu') }
    )
    

    lo dejas en:

    /* Beginning of the process */
    toggleMenuH.addEvent ( 
    window, 'resize', function(){ toggleMenuH.equalWidth('ul','first_menu', 'second_menu') }
    )
    

    Y la líneas que hemos quitado la movemos a la página web, que en el ejemplo quedaría de este modo:

    <head>
      <title>Menu horizontal basado en el menu de Roger Johansson - v1.0</title>
      <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
      <style type="text/css" media="screen">
        @import "css/estilo.css";
      </style>
      <script type="text/javascript" src="js/hmenu.js"></script>
      <script type="text/javascript">
      /* Beginning of the process */
      toggleMenuH.addEvent(window, 'load', function(){
        toggleMenuH.init('first_menu', 'hidden', 'span', 'select', 2);
        toggleMenuH.equalWidth('ul','first_menu', 'second_menu');
      })
      </script>
    </head>
    

    Ahora sólo te queda modificar el documento, que en tu ejemplo es procomel.html (pero que me imagino sea un procomel.php o algo por el estilo), para que en función de en qué sección de la web esté cambie el valor del último argumento de la función toggleMenuH.init() (es decir, del 2).

    ¿Te vale con esto? Si no es así dime lo intentamos con más ganas 🙂

  16. Hola Jorge..solucionado a medias..la segunda parte de desplegar según pagina el submenu funciona..pero seguimos teniendo problemas con los link de primer nivel..eso nada. al hacer clink solo abre o cierra los submenus y esto tiene que ser por algo del hmenu.js, por curiosida y probando en algunos casos me ha funcionado añadiendo lo siguiente window.location del addEvent.pero me despliega tambien los otros submenus.. 5 horas llevo con esto hoy.

  17. Hola pues nada amigo sigue sin funcionar.. vamos a hacer lo siguiente, voy a montar un ejemplo funcional y te voy a pasar los ficheros fuentes, nosotros lo vamos a colgar en web para ver los cambios si te parece.. este modo de menu 2 niveles está muy buscando en internet y solo hemos encontrado tu solución..ahora te aviso cuando esté.. un saludo.

  18. Me alegro de que funcione
    Como te dije en el último comentario, lo único que hice fué cambiar el fichero jmenu.js para que diferenciase entre los enlaces normales y los de javascript. Me imagino que haya sido eso 🙂

  19. Hola Jorge, veo que este post tiene varios años, pero quería saber si todavía podes pasarme los archivos CSS y js. Estoy necesitando hacer un menú similar a este, en realidad necesito de 3 niveles, así que si tenes algo parecido te lo agradezco. Muchas gracias. Saludos
    Gabriel

  20. Gracias Jorge, no sabes lo bien que me viene!. Pruebo y te comento, pero desde ya, gracias, gracias y más gracias!
    Saludos
    Gabriel

  21. Gracias Jorge, funcionó perfecto, ahora voy a tratar de agregar un nivel más, si me trabo en algún lado te voy a molestar otra vez!.
    Saludos
    Gabriel

  22. Hola Jorge, nuevamente solicitando tu ayuda. Hasta ahora el menú funcionó perfecto, pero por algo particular de mi desarrollo tuve que hacer que el primer menú ejecute un link a una url especifica y el submenu unas funciones. El problema que tengo es que el primer menú ejecuta el link solo si no tiene submenus. Me podrías ayudar con esto, estuve haciendo algunos cambios en el código (donde yo creía que estaba el problema) pero fue peor!. Así que prefiero preguntar. Muchas gracias.
    Saludos
    Gabriel

Leave a Reply

Your email address will not be published. Required fields are marked *