Le soluzioni basate esclusivamente su CSS rappresentano un'ottima scelta
per la costruzione di menu gerarchici di navigazione all'interno
di un sito web: importanti vantaggi dati dal loro uso riguardano soprattutto gli
aspetti legati all'accessibilità, poiché sono costruiti in modo semanticamente
corretto (mediante liste non ordinate), non richiedono plugin o supporto Javascript
del browser e, conseguenza naturale, consentono spesso
un caricamento più veloce della pagina rispetto ad altre soluzioni ibride
(CSS + Javascript) oppure completamente realizzate in Javascript o Flash.
Una soluzione di questo tipo necessita tuttavia di una buona conoscenza
dell'uso dei fogli di stile, soprattutto per via dei commenti condizionali necessari per
il corretto funzionamento su Internet Explorer. Alcuni menu però (soprattutto quelli a due soli
livelli di navigazione) non sono estremamente complessi e in rete è possibile
trovare diverse risorse da cui poter attingere, tra le quali citiamo CSSPlay creato da
Stuart Nicholls (http://www.cssplay.co.uk/) e dal cui sito prenderemo un semplice menu
di esempio per questo articolo.
è necessario sottolineare che, sebbene questi menu presentino i vantaggi elencati
in precedenza, per la natura del loro funzionamento, non possono tenere in considerazione
un importante aspetto, quello dell'usabilità.
Com'è noto, il meccanismo dei menu CSS si basa fondamentalmente sulla pseudoclasse
:hover
(sugli elementi <a>
in Internet Explorer 6 ed inferiori, sui list-items <li>
per tutti gli altri browser).
Il tempo di risposta dell'evento da parte dei vari user-agent è pressoché immediato, il che
significa che se l'utente, portando il mouse da una voce di primo livello alle voci di secondo
di livello, esce inavvertitamente dall'area gestita dalla pseudoclasse, le voci di secondo
livello si chiuderanno (o cambieranno) istantaneamente.
Ciò costringerà l'utente a riposizionare il mouse nuovamente sul menu ed effettuare una seconda
volta la selezione della voce. Per molti questa potrà sembrare solo un'operazione ripetitiva ma, per chi
non usa abitualmente il web o per persone con ridotta capacità d'attenzione, questa situazione può
diventare frustrante e rappresentare un ostacolo alla fruizione dei contenuti.
A tale proposito possiamo intervenire attraverso Javascript non intrusivo - ed in modo relativamente
semplice - per migliorare la user-experience. Il nostro scopo sarà quindi quello di ritardare
di un certo quanto di tempo - definibile a piacere - l'evento onmouseover
(e,
analogamente, quello di onmouseout
) in modo tale da tollerare brevi istanti nei quali l'utente possa
uscire dall'area gestita dall'hover di una determinata voce di primo livello. Ecco la demo.
Consideriamo il seguente menu a due livelli (il cui codice, per la comprensione dell'articolo, è riportato
al minimo indispensabile).
XHTML
<ul class="css_menu" id="css_menu_id1"> <li class="current"><a href="#" rel="first-level">Voce 1 <!--[if gte IE 7]><!--></a><!--<![endif]--> <!--[if lte IE 6]><table><tr><td><![endif]--> <ul> <li><a href="#">sottovoce 1_1</a></li> <li><a href="#">sottovoce 1_2</a></li> <li><a href="#">sottovoce 1_3</a></li> </ul> <!--[if lte IE 6]></td></tr></table></a><![endif]--> </li> <li><a href="#" rel="first-level">Voce 2 <!--[if gte IE 7]><!--></a><!--<![endif]--> <!--[if lte IE 6]><table><tr><td><![endif]--> <ul> <li><a href="#">sottovoce 2_1</a></li> <li><a href="#">sottovoce 2_2</a></li> <li><a href="#">sottovoce 2_3</a></li> </ul> <!--[if lte IE 6]></td></tr></table></a><![endif]--> </li> </ul>
Codice CSS (le sole regole sulle quali ci soffermeremo)
<style type="text/css"> /* ...[omissis]... */ .css_menu li.current ul, .css_menu li.jshovered ul, .css_menu li.jshovered:hover ul, .css_menu li.jshovered a:hover ul { display : block; position : absolute; z-index : 10; height : 1.5em; top : 1.5em; left : 0; background : #fff; } .css_menu li.jshovered:hover ul { z-index : 20; } </style> <!--stile da rimuovere se javascript è attivo --> <style type="text/css" id="purecssmenu"> .css_menu li:hover ul, .css_menu a:hover ul { display : block; position : absolute; z-index : 20; height : 1.5em; top : 1.5em; left : 0; background : #fff; } </style>
Mentre il codice Javascript che ci consentirà di ottenere il risultato voluto è il seguente:
<script type="text/javascript"> //<![CDATA[ var DelayMenu = function(idmenu) { /* Private Members */ var _hoverClass = 'jshovered'; var _hoverDelay = 300; var _hoverLi = []; var _hoverIntv; /* Private Methods */ var _$ = function(id) { return (document.getElementById) ? document.getElementById(id) : document.all(id); }; var _addClass = function(el, c) { // ...[omissis]... }; var _removeClass = function(el, c) { // ...[omissis]... }; return { /* Privileged Methods */ setHoverClass : function(hc) { if (typeof hc === 'string' && (/^w[wd]*$/).test(hc)) { _hoverClass = hc; } }, setHoverDelay : function(hd) { if (!isNaN(hd)) { _hoverDelay = hd; } }, /* Public Method (constructor) */ init : function() { /* remove pure css approach */ if (_$('purecssmenu')) { var cssrules = _$('purecssmenu'); cssrules.parentNode.removeChild(cssrules); }; // collect all first-level list-items var mLinks = _$(idmenu).getElementsByTagName('a'); for (var i=0; i<mLinks.length; i++) { if (mLinks[i].rel === 'first-level' && mLinks[i].parentNode.className !== 'current') { _hoverLi[_hoverLi.length] = mLinks[i].parentNode; } }; /* Set onmouseover/onmouseout events for timed delay */ for (var i=0; i<_hoverLi.length; i++) { var li = _hoverLi[i]; li.onmouseover = (function(i) { return function() { var _thisLi = this; clearInterval(_hoverIntv); _hoverIntv = setTimeout(function() { for (var j=0; j<_hoverLi.length; j++) { _removeClass(_hoverLi[j], _hoverClass) }; _addClass(_thisLi, _hoverClass); }, _hoverDelay); } })(i); li.onmouseout = (function(i) { return function() { var _thisLi = this; clearInterval(_hoverIntv); _hoverIntv = setTimeout(function() { _removeClass(_thisLi, _hoverClass) }, _hoverDelay); } })(i); } } /* end init function */ } /* end return statement */ }; window.onload = function() { var myApp = {}; myApp.myMenu = new DelayMenu('css_menu_id1'); myApp.myMenu.init(); }; //]]> </script>
L'oggetto DelayMenu
contiene alcune proprietà private, alcuni metodi privati che ci consentono di
avere utili shortcuts per operazioni ricorrenti, come addclass
e removeclass
(sui quali non
ci soffermiamo, il loro significato dovrebbe essere chiaro), metodi privilegiati (per cambiare
le proprietà private) e un metodo pubblico 'init
': analizziamolo in dettaglio.
Il primo costrutto 'if
', controlla la presenza dell'ultima parte di codice CSS (quella cioè
deputata all'apertura del secondo livello del menu tramite semplice :hover
) e quindi la
rimuove, in modo da non interferire con il restante codice Javascript.
A questo punto le regole CSS restanti sono quelle che gestiscono l'evento :hover
, ma
solo in presenza di list-items (<li>
) che hanno una classe chiamata 'jshovered
'.
Nel ciclo 'for
' seguente valorizziamo un array con i riferimenti ai list-items di primo livello
(identificati anche grazie all'attributo 'rel
' dei link annidati, definiti appositamente a
tale scopo). Nell'array escludiamo anche quei list-items che hanno classe 'current
', ovvero
quelli che sono già aperti (perchè ad esempio ci troviamo in una pagina di quello specifico
livello) e che non necessitano di essere gestiti via Javascript.
Dopodiché cicliamo sulla lunghezza dell'array per definire gli eventi 'onmouseover
'
e 'onmouseout
' (sui list-items stessi), rispettivamente in modo da aggiungere e rimuovere
la classe 'jshovered
' (nota: la sintassi con cui sono state dichiarate queste funzioni garantisce
una corretta implementazione delle closures e l'assenza di memory leaks su IE6).
Si noti che tali eventi sono controllati attraverso un setTimeout
, il cui intervallo in millisecondi
è impostabile attraverso l'apposito metodo privilegiato 'setHoverDelay
'. Ogni volta che si verifica
uno di questi due eventi un eventuale timeout settato in precedenza viene eliminato.
Infine, all'evento onload della pagina (meglio se gestito con un DOMLoad), istanziamo l'oggetto
'DelayMenu
', settando un eventuale delay diverso da quello di default (scelto empiricamente in 300ms)
e/o il nome della classe da settare/rimuovere. Il codice, così realizzato, ci consente
quindi di avere più menu nella stessa pagina, ognuno dei quali sarà gestito da un'apposita istanza
dell'oggetto (al quale dev'essere passato solo l'id del menu come argomento).
La compatibilità di questo script è stata testata su Internet Explorer 6 e 7, su Firefox 2+ per PC e Mac,
su Firefox 3/Ubuntu Linux e su Safari 3.0.4 per Mac.
Come utile esercizio si lascia al lettore la facoltà di apportare altre modifiche al codice, ad esempio
per poter gestire lo stesso effetto per menu a 3 o più livelli oppure per disattivare il ritardo di apertura
di un sottolivello se un altro è già aperto e così via.