Usar jquery.noConflict en plantillas html pre-procesadas

Si alguna vez necesitáis no usar $ para jquery, porque estáis usando una plantilla HTML que debe ser pre-procesada antes de mostrarse y el procesador usa esa misma variable, podéis hacer uso de jquery.noConflict(). A la luz de uno de sus ejemplos:

// http://api.jquery.com/jquery.noconflict/
$.noConflict();
jQuery( document ).ready(function( $ ) {
// Code that uses jQuery's $ can follow here.
});

podemos cambiarlo para no usar $ (que, repito, es intepretada por el procesador de la plantilla):

jQuery.noConflict();
jQuery( document ).ready(function( __ ){
// código que usa __ en lugar de $
__('body').addClass('no-backbround');
});

Seguro que muchos ya lo sabíais pero lo dejo para los que no 😉

timelineJS o cómo pasárselo bien durante horas.

Todo aquel que haya hecho, o haga, desarrollo web sabe una cosa: nunca pienses que la web será vista como la ves tú. A pesar de que en la fase de desarrollo, y test, se utilicen todos los navegadores del mercado algún detalle te saltará cuando menos te lo esperes.

Pues esto ha sido lo que nos ha llevado de cabeza dos dias. Para mostrar cierta información, en un reciente desarrollo, nos hemos decantado por el timelineJs de Verite.co. Tras echarle un ojo, ver cómo se integra en un desarrollo en marcha y sus posibilidades nos ponemos a ello y lo insertamos. Todo parece funcionar bien: Firefox ok, Chrome ok, Opera ok,.

Confiadamente pasamos al siguiente tema y, de inmediato, un compañero nos dice: “pues en Internet explorer no se ve”. ¡¡Venga hombre, qué dices!! Tendrás una versión vieja (mentira porque todos tenemos la misma), tendrás desactivada alguna opción o será problema de la cache de tu IE porque nosotros lo vemos bien … mira, ven, que en mi ordenador se ve ….mal!! al abrirlo con mi IE tampoco funciona!!! Dios, pero quien me manda a mi meterme en esto de la Web, si estaba mejor haciendo calceta.

Con la idea de solventarlo en dos minutos me pongo a ello. Abro la página en mi Firefox (tuneado con las extensiones webdeveloper y firebug)

https://www.example.com/timeline.html

y al mirar la consola no veo errores. Repito la operación en IE y nada. Sin verlo muy claro comienzo a buscar en Internet a ver si a alguno le ha pasado algo parecido. Nada, no hay nada. Activamos el modo debug en el timeline y vuelvo a examinar las consolas (la de Firebug y la de las Herramientas de Desarrollador de IE) y veo en la de Firebug salen un montón de mensajes (con un correcto funcionamiento del timeline) y que en IE se para enseguida con estos mensajes:

IE JSON
Google Docs ERROR: timeout
Google Docs ERROR: 

Bufff, toca remangarse y revisar el código. Modifico el timeline para que use timeline.js en lugar de su versión minimizada; busco el mensaje “IE JSON” y veo esto en su línea 536:

VMM.getJSON = function(url, data, callback) {
  if( typeof( jQuery ) != 'undefined' ){
    jQuery.ajaxSetup({
      timeout: 3000
    });
    /* CHECK FOR IE
    ================================================== */
    if ( VMM.Browser.browser == "Explorer" && parseInt(VMM.Browser.version, 10) >= 7 && window.XDomainRequest) {
      trace("IE JSON");
      var ie_url = url;
      if (ie_url.match('^http://')){
        return jQuery.getJSON(ie_url, data, callback);
      } else if (ie_url.match('^https://')) {
        ie_url = ie_url.replace("https://","http://");
        return jQuery.getJSON(ie_url, data, callback);
      } else {
      return jQuery.getJSON(url, data, callback);
      }

    }  else {
      return jQuery.getJSON(url, data, callback);
    }
  }
}

Busco Google Docs ERROR y veo:

  model: {

  googlespreadsheet: {

    getData: function(raw) {
      var getjsondata, key, url, timeout, tries = 0;

      key  = VMM.Util.getUrlVars(raw)["key"];
      url  = "https://spreadsheets.google.com/feeds/list/" + key + "/od6/public/values?alt=json";

      timeout = setTimeout(function() {
        trace("Google Docs timeout " + url);
        trace(url);
        if (tries < 3) {
          VMM.fireEvent(global, VMM.Timeline.Config.events.messege, "Still waiting on Google Docs, trying again " + tries);
          tries ++;
          getjsondata.abort()
          requestJsonData();
        } else {
          VMM.fireEvent(global, VMM.Timeline.Config.events.messege, "Google Docs is not responding");
        }
      }, 16000);

    function requestJsonData() {
      getjsondata = VMM.getJSON(url, function(d) {
        clearTimeout(timeout);
        VMM.Timeline.DataObj.model.googlespreadsheet.buildData(d);
      })
      .error(function(jqXHR, textStatus, errorThrown) {
        trace("Google Docs ERROR");
        trace("Google Docs ERROR: " + textStatus + " " + jqXHR.responseText);
        })
      .success(function(d) {
        clearTimeout(timeout);
      });
    }
    
    requestJsonData();
    },

Con estas pistas me pongo a examinar qué pasa con el documento de Google Docs que se usa como fuente de datos. Su url es del tipo:

https://docs.google.com/ruta_larga_al_documento&format=html

En la línea 14, del primer código, veo que se hace una cambio de protocolo: si el documento está en una ruta con protocolo https se modifica para que se solicite por http . ¿Por qué? no lo pensé en su momento. Solicité en el navegador el documento con protocolo http a ver qué pasaba y vi que Google hace una redirección permanente, un header 302, a la versión https del documento y añade dos headers más:

x-frame-options=SAMEORIGIN
x-xss-protection=1; mode=block

¿Serían estos dos headers quienes estaban causando problemas? Tras leer un rato sobre el tema

Me digo que no y me vuelvo a centrar en el código de timelineJS. Releo el código javascript con más calma:

else if (ie_url.match('^https://')) {
  ie_url = ie_url.replace("https://","http://");
  return jQuery.getJSON(ie_url, data, callback);
}

Si pedimos, con IE, un documento https, timelineJS, lo cambia a https y llama a jQuery.getJSON. Veamos qué dice la documentación sobre la función. Nada nuevo hasta que llego a las notas adicionales:

Due to browser security restrictions, most “Ajax” requests are subject to the same origin policy; the request can not successfully retrieve data from a different domain, subdomain, or protocol.

Por fín, la respuesta estaba ahí. Si nuestra página se ejecuta sobre https no se puede hacer una petición ajax vía http, y viceversa. Tras más de 5 horas teníamos la solución, para que luego digan que esto de hacer Web no es divertido 🙂

Mapa de Google señalando varios puntos mediante jQuery y/o Prototype

Hace unos días que estoy para escribir este artículo pero por una cosa u otra se me queda en el tintero. Pero de hoy no pasa así que aquí va.

A raiz de intentar incluir el módulo GMap de Drupal, en un proyecto reciente, me vi en la necesidad de generar una nueva clave para incluir un mapa de google. Cual fué mi sorpresa al ver que esto ya no era posible desde la publicación de la versión 3 de la API de Google Maps. Tras dar alguna que otra vuelta por la documentación de esta conseguí crear el ejemplo básico de un mapa de Google usando API v3  ( lo cierto es que es bastante sencillo de crear y nos evitan el tener que crear la clave para cada dominio donde alojemos el mapa).

Pero me quedó el gusanillo de poder tener un mapa con varios puntos así que me puse a ello: teniendo un documento XML con los puntos y sus localizaciones:

<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
<!-- http://geoco.org/espana-es.html -->
<puntos>
	<punto>
		<titulo><![CDATA[Santander]]></titulo>
		<desc><![CDATA[Santander, home sweet home]]></desc>
		<url>http://www.ayto-santander.es</url>
		<latitud>43.464</latitud>
		<longitud>-3.8</longitud>
	</punto>	
	<punto>
		<titulo><![CDATA[Barcelona]]></titulo>
		<desc><![CDATA[Barcelona, capital de Cataluña]]></desc>
		<url>http://www.bcn.es/</url>
		<latitud>41.38</latitud>
		<longitud>2.18</longitud>
	</punto>	
	<punto>
		<titulo><![CDATA[Mérida]]></titulo>
		<desc><![CDATA[Cuidad monumental y arqueológica]]></desc>
		<url>http://www.merida.es/</url>
		<latitud>38.92</latitud>
		<longitud>-6.33</longitud>
	</punto>		
</puntos>

los cargaría con javascript ( la primera opción, no os mentiré, fue hacerlo con Prototype -porque tenía un ejemplo de lectura de un XML hecho con él, y luego con jQuery) y los mostraría en un sencillo documento HTML.

Carga de los datos con Prototype.

Si véis el contenido del fichero javascrpipt que realiza el proceso, xml.js , veréis algo como esto:

function obtenerPuntos( )
{
	var url = 'js/puntos.xml';
	var myAjax = new Ajax.Request( url, { method: 'get', onComplete: renderResultsPuntos });
}

Que no es otra cosa que la lectura del fichero XML, con los datos, mediante Ajax y la asignación de esta lectura al objeto myAjax que se utiliza en la función que pinta el mapa, renderResultsPuntos().

function renderResultsPuntos(responseXMLSec)
{
	var myZona = new Array();
	var marker = new Array();
	var contentString = new Array();
	var infowindow = new Array();
	var xml  = responseXMLSec.responseXML.documentElement;
	var miDiv = document.getElementById('puntos-place'); 
	
	var ptoMadrid = new google.maps.LatLng(40, -3.5); // Centramos el mapa en Madrid.
	var myOptions = {
		zoom: 6,
		center: ptoMadrid,
		mapTypeId: google.maps.MapTypeId.ROADMAP
	}
	var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
	for (i = 0; i < xml.getElementsByTagName('punto').length; i++)
	{
		var item = xml.getElementsByTagName('punto')[i]; // Accedemos al objeto XML seccion
		var urlDir = item.getElementsByTagName('url')[0].firstChild.data;
		var titulo = item.getElementsByTagName('titulo')[0].firstChild.data;
		var texto = limpiaStr(item.getElementsByTagName('desc')[0].firstChild.data);
		
		myZona[i] = new google.maps.LatLng(
item.getElementsByTagName('latitud')[0].firstChild.data,
item.getElementsByTagName('longitud')[0].firstChild.data); 
		contentString[i] = '<div class="google-content">'+
			'<h2>' + titulo + '</h2>'+
			'<div class="bodyContent">'+
			'<p>' +  texto + '</p>'+
			'<p>URL: <a href="'+urlDir+'">' +  urlDir + '</a></p>'+
			'</div>'+
			'</div>';
		infowindow[i] = new google.maps.InfoWindow({ content: contentString[i] });
		marker[i] = new google.maps.Marker({ position: myZona[i], map: map, title: titulo });
		eval ( "google.maps.event.addListener( marker[" + i + "], 'click', function() { infowindow["+i+"].open( map, marker["+i+"] ); })");
	}
}

Con lo que la creación de un mapa de Goole con Prototype queda completa.

Carga de los datos con jQuery.

En el caso de crear el mapa de Google con jQuery, ejecutamos el proceso dentro de la función, pintaMapa(), que realiza la petición Ajax al fichero de datos y, cuando esta se haya completado, pinta el mapa, ver fichero jquery.xml.

var myZona = new Array();
var marker = new Array();
var contentString = new Array();
var infowindow = new Array();

$(document).ready(function() {  
	pintaMapa("map_canvas");
});

// general options for all ajax request
$.ajaxSetup({
	type: "GET",
	statusCode: {
		404: function() {
			alert("page not found");
		},
		500: function() { 
			alert("Server error");
		}
	}
});
function pintaMapa(destID) {
	
	if ( $('#' + destID).length = 0 ) { return;}
	$.ajax({
		dataType: 'text html',
		url: 'js/puntos.xml'
	})
	.done(function(data){
		xmlDoc = $.parseXML( data );
		$xml = $( xmlDoc );
		$puntos = $xml.find( "punto" );
		if  ( $puntos.length > 0 ) {
			var ptoCentro = new google.maps.LatLng(40, -3.5); // Madrid, aprox!!!
			var myOptions = {
				zoom: 6,
				center: ptoCentro,
				mapTypeId: google.maps.MapTypeId.ROADMAP
			}
			var map = new google.maps.Map(document.getElementById(destID), myOptions);
			var i = 0;
			$.each ( $puntos, function() {
				var titulo = $(this).find("titulo").text();
				var url = $(this).find("url").text();
				myZona[i] = new google.maps.LatLng($(this).find("latitud").text(), $(this).find("longitud").text()); 
				contentString[i] = '<div class="google-content">'+
					'<h2>' + titulo + '</h2>'+
					'<div class="bodyContent">'+
					'<p>' +  $(this).find("desc").text() + '</p>'+
					'<p>URL: <a href="'+url+'">' +  url + '</a></p>'+
					'</div>'+
					'</div>';
				infowindow[i] = new google.maps.InfoWindow({ content: contentString[i] });
				marker[i] = new google.maps.Marker({ position: myZona[i], map: map, title: titulo });
				eval ( "google.maps.event.addListener( marker[" + i + "], 'click', function() { infowindow["+i+"].open( map, marker["+i+"] ); })");
				i++;
			});
		}
	});
}

Una de las diferencias entre generar el mapa con Prototype o con jQuery es que en este segundo caso las variables donde se almacenan los puntos deben ser globales a todos el script. Antes de acabar deciros que  seguro que el codigo fuente es muy mejorable pero por lo menos funciona.

Imprimir en Opera con javascript

Hoy me he enterado, dicen que más vale tarde que nunca, que la estándar de imprimir con javascript:

window.print();

no funciona siempre en Opera. Para que en lo haga hay que tener en cuenta que la página debe estar completamente cargada. Si esto se cumple, el siguiente ejemplo funciona:

<input type="button" value="Print this page" onClick="javascript:window.print();" ID="Button1" NAME="Button1">

El problema viene cuando antes de imprimir necesitamos hacer algo sobre el HTML ( abrimos una nueva ventana, modificarlo, etc). Tomemos este ejemplo, en el que en la función ocultar() debe hacer cosas previas a la impresión.

function imprimePagina(){
    ocultar(); // funcion que necesita ocultar información que no debe ser impresa.
    window.print();
    // resto de código;
}

Este ejemplo funciona en IE, Firefox, Chrome pero no en Opera. Para solventarlo podríamos probar con:

function imprimePagina() {
    ocultar(); // funcion que necesita ocultar información que no debe ser impresa.
    window.setTimeout(function () {
        window.print();
    }, 5);
    // resto de código;
}

Que, si no he escrito mal el ejemplo, debiera funcionar. En el caso de que abriéramos una ventana hemos de recordar lo que comentamos al principio: la página, la nueva, debe estar completamente cargada antes de intentar imprimirla en Opera. Como no controlamos en qué momento sucede esto, podemos confiar en el evento load de la ventana para imprimirla.

window.onload = function () {
    window.setTimeout(function () {
        window.print();
    }, 500);
}

Espero que a alguno le valga de algo 🙂

Referencia: http://stackoverflow.com/questions/3482428/window-print-not-working-with-opera-browser

La ética del desarrollo Web frente a las técnicas del posicionamiento

Os voy a contar un secreto que no muchos de los que se dedican al desarollo Web cuentan, y no me incluyo entre ellos porque nunca supe demasiado del tema y porque cada día juego menos con la Web: no sé mucho de SEO (o mejor dicho, Optimización para los motores de búsqueda). ¿Y por qué os cuento esto?

Hace unos días un amigo me preguntó por qué cuando en Google (por decir un buscador) buscaba palabras muy relacionadas con su empresa, en su ciudad, había comenzado a aparecer, a la misma altura que su Web o mejor dependiendo de la combinación de palabras, la Web de la empresa de uno de la competencia. Curioso que es uno, me puse a investigar qué era lo que tenía de mágico esa nueva Web que tanto lo preocupaba.

A simple vista la Web era bastante sencilla en su diseño y escasa en cuanto a su contenido. Intentando aplicar algunas de las nociones de SEO que poseía (densidad de palabras, textos bien redactados, estructura sencilla, etc) no veía cómo había conseguido posicionar esa Web donde estaba. Tras un rato de investigar y juntar las piezas llegué a esta conclusión: la Web en cuestión era un desarrollo a medida, creado por una empresa de guías teléfonicas de españa (QDQ – http://es.qdq.com/), para promocionar algunos de sus clientes. En este desarrollo a medida he visto varias cosas relevantes:

  1. Si es necesario se registra un nuevo dominio para el cliente, con un nombre más pensado en los motores de búsqueda que en el nombre real del Cliente.
  2. Se utiliza javascript para modificar al vuelo los enlaces internos de la Web para, en ellos, incluir el nombre del cliente y así potenciarlo para los buscadores.
  3. El sitio Web creado es tan sencillo en su texto que más bien parece únicamente pensado para los buscadores que para las personas.

No voy a entrar en si el modo técnico de hacerlo es mejorable o no (aquí os dejo una prueba de concepto de cómo modificar los enlaces con javascript manteniendo la navegación), voy a hacerlo en las técnicas usadas. Todos sabemos que para que nuestra página aparezca en los primeros puestos es necesario gustarle a los buscadores, en eso radica el bello arte del desarrollo Web. También hemos leido sobre el oscuro poder que tienen los enlaces externos hacia nuestra Web (nadie sabe cuánto poder pero por si acaso más vale conseguirlos) y la densidad de palabras de nuestras páginas. Pero, os pregunto, ¿tan necesarios son como para que una empresa se dedique a crear páginas Web pensadas para buscadores (como ellos mismos venden) más que para personas? ¿Y por qué digo esto?

Porque una Web en la que el nombre de la empresa aparece en los encabezados de primer nivel, de segundo y tercer nivel (una vez como h1, dos veces como h2 y una vez como h3) no me parece que respete mucho la estructura semántica del documento. Porque vale que la dirección sea importante, ¿pero como para que sea un encabezado de tercer nivel? Porque, en mi opinión, una Web en la que la media de palabras (que no sean conjunciones, preposiciones o artículos) oscile sobre las 60 no la considero demasiado informativa para un lector humano. Vale que un texto breve bien redactado puede transmitir mucha información pero me da a mi que en el caso de las Webs creadas como el ejemplo esto no ocurre. Así todo os dejo al final un PDF con la ejecución de SeoQuake sobre un dominio de los creados por QDQ.

Analisis de la Web de ejemplo mediante Fangs
Análisis de la Web de ejemplo mediante Fangs


Estas técnicas de mejorar la densidad de palabras, la importancia de las mismas y de ganar enlaces externos me recuerdan a las viejas, y penalizadas alguna de ellas, técnicas de intercambio de links y a la inclusión de textos ocultos para el usuario humano pero no para el buscador. Ojo, no veo mal que una empresa invierta dinero en publicitar su Web, ¿pero hacerlo de este modo? Creando una web paralela, apoyada por los enlaces externos generados por un directorio de servicios como es QDQ (nada criticable en esto) pero tan pensada para los buscadores que olvidan al usuario. ¿es realmente necesario registrar nuevos dominios con nombres que no tengan casi que ver que el verdadero nombre del cliente? Ej: se registra un dominio nuevo "gimnasio-fitness-madrid.es" para el cliente "Work Station Fitness Center", ¿alguno le ve la relación? Imaginad que se impone esta forma de trabajar, ¿en cuánto tiempo nos quedaremos sin nombres coherentes para los dominios? ¿Tendrá sentido encontrar webs de fruterías cuyo dominio hable de gimnasios?

Puede que alguno no entienda lo que digo, otros que no lo compartan pero de todos vosotros me gustaría una respuesta. Se me olvidaba, un ejemplo de estas Webs a medida es http://www.gimnasio-fitness-madrid.es/es/presentacion/

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

Esquinas redondeadas, o como hacer fácil lo dificil

Esta semana, en el trabajo, me pasaron un diseño 'sencillo y rápido' que cuanto más miraba más degradados, esquinas redondeadas y 'detallitos varios' veía.

Hasta la fecha las dichosas esquinas las he hecho con imágenes (bien incluyendolas en celdas, como fondo de las mismas, o dentro de algún elemento HTML) pero debido a la cantidad de ellas que tenía el diseño, me decidí por buscar algo en Internet.

Por casualidad, estaba mirando el correo 🙂 y vi un mensaje en la Lista de Ovillo que hablaba precisamente del tema de las esquinas redondeadas. Entre todos los mensaje unos se decantaban por utilizar imágenes para hacerlas, y CSS para posicionarlas, y otros por CSS y Javascript. Como no veía muy clara la diferencia me puse a leer un rato sobre el tema, y trás un rato, me decanté por este último.

¿Por qué? Porque utilizando el método de las imágenes + CSS tienes que crear un número de imágenes igual a cuatro veces el número de elementos cuyas esquinas quieras redondear. Porque para hacer el posicionamiento, el primero método, utilizaba bastante elementos HTMl 'sin cerrar', produciendo con ello errores en la semántica del documento, como:

	<p class='clase'><b class='clase2'></b></p>

Y, porque después de reflexionar, me acordé que de el método CSS+Javascript ya me había hablado mi gurú personal en esto del CSS y la accesibilidad, Marco Giacomuzzi

El método de creación de esquinas redondeadas con CSS+Javascript ha sido desarrollado por Alessandro Fulciniti y os remito a ella para ver toda la potencia del método. No lo dudéis y visitadla.

A pesar de que os voy a dar un ejemplo sencillo del uso del método de Alessandro, que él llama Nifty Corners, os recomiento encarecidamente que visitéis su web.

En el ejemplo vamos a redondear un div, llamado 'box'. Lo primero que hacemos es declarar el div a redondear:

	<div id="box">
		<h1>Nifty Corners Cube&trade;</h1>
		<p>Se acabaran las ñapas</p>
	</div>

El siguiente paso es incluir la llamada a la función que se encargará del redondeo del div (para ver las opciones visita la página del autor). Recuerda que antes de hacer la llamada, a la función que realiza el redondeo, has de incluir el fichero javascript que con tiene a dicha función, Nifty().

	<script type="text/javascript">
		window.onload=function(){
			Nifty("div#box");
		}
	</script>

Para acabar el proceso, dale un color de fondo al elemento a redondear desde la hoja de estilos.

	div#box{
		width: 18em;
		padding: 20px;
		margin:0 auto;
		background:#FFA509;
		color:#000;
	}

Desde este momento debieras estar viendo un div en medio de la pantalla con las esquinas redondeadas. Si por cualquier motivo no funcionase, verifica que las rutas de los ficheros que se incluyen -requiren- estén bien. Además, has de vigilar que el fichero niftyCorners.CSS esté donde indican las líneas de la función AddCSS(), que tienes en la línea 32 del fichero niftycube.js

Recuerda, con estos tres sencillos pasos has redondeado un elemento HTML:

  1. Creación del elemento a redondear.
  2. Llamada a la función de redondeo, Nifty()
  3. Definición el estilo del elemento a redondear.

Espero que el método os sea tan útil como me ha sido a mi