A distanza di quasi due mesi dall'ultimo script pubblicato, arriva finalmente il momento di un nuovo script.
L'occasione è data dalla necessità di sostituire il "Menu a discesa" di Fabiano: ormai ho assodato (grazie a numerose e-mail di richiesta d'aiuto) che non vale la pena di metterci mano per modificarlo, soffre di alcuni bug che comporterebbero comunque la riscrittura.
Incalzato da una ventina di e-mail solo negli ultimi quattro mesi, ho ripreso il mitico
Menu a tendine che mi offriva già una solida e collaudata base di array e funzioni di contorno e ne ho ricavato un nuovo menu a discesa.
Ve lo presento in questa scheda che mi ha consentito, finalmente, di riprendere a scrivere per JsDir.com come ho sempre voluto e nel modo che più mi piace: cioè non lasciare uno script da personalizzare e scopiazzare (giò lo fanno in
troppi!!!), ma cercando di insegnare qualcosa e fare di uno StaffScript non solo uno script ma, sopratutto, un
tutorial!
Auguro buona lettura a chi avrà la pazienza e l'interesse a seguirmi in questo lungo pezzo, dal canto mio ci ho lavorato, alla fin dei conti, un giorno intero, ma spero che ne sia valsa la pena; come al solito se trovate qualche strafalcione sarò ben lieto di correggermi.
Il menu fa ampio uso della struttura e degli script che molti hanno già visto per il
Menu a tendine.
Di questo infatti eredita le classi di stile usate per l'aspetto grafico dei menu, gli array per i links e buona parte delle funzioni di contorno.
Se si confrontano i due script ci si accorge infatti che le uniche parti che hanno subito cambiamenti sostanziali, sono state le variabili di posizionamento, e le funzioni che gestivano l'apertura delle tendine, qui sostituite da un'unica funzione (
DDMenuApri()); tutto il resto è pressoché immutato.
Lo script è suddiviso in due parti: una da configurare per la personalizzazione ed una da lasciare immutata; è quest'ultima la parte "attiva" del menu ed andrebbe modificata solo se si è abbastanza esperti di JavaScript.
Per la parte da personalizzare vi rimando alla descrizione del
Menu a tendine per quanto riguarda gli array delle voci del menu, che sono praticamente indentici in tutti e due gli script. Idem per le classi del foglio di stile che andranno configurare come qualsiasi css.
La classe per il tag "a" è stata inserita nell'esempio a solo scopo dimostrativo, anche perché viene usata, di fatto, solo per la parte che concerne Netscape 4 in cui è necessario usare il link per eseguire l'apertura dei livelli, per tutti gli altri browser è l'event-handler
onclick che si occupa della gestione del menu.
Di tutto lo script la parte più importante è la funzione
DDMenuApri() che gestisce i livelli.
Ed è proprio questa, insieme alle variabili di contorno, che sarà oggetto della parte più importante di questa scheda.
Vediamone il codice:
var nn=document.layers?true:false
var w3c=document.getElementById?true:false
var last=-1;
if (nn)
{
for (var i = 0 ; i<voci.length; i++)
{
document.write("<layer ... name='pr"+i+"' ...
for (var ii = 1 ; ii < voci[i].length ; ii++ )
document.write("<layer ... name='sc"+i+"_"+ii+"' ...
}
}
else
{
. . . . . .
}
var beg=nn?"document.layers":w3c?"document.getElementById(":"document.all";
var mid=nn?"":w3c?").style":".style";
function DDMenuApri(quale)
{
for (var i=0 ; i<voci.length ; i++ )
{
eval(beg+"['pr'+"+i+"]"+mid+".top=eval(top+alto*"+i+"); ")
for (var ii = 1 ; ii < voci[i].length ; ii++)
eval(beg+"['sc'+"+i+"+'_'+"+ii+"]"+mid+".visibility='hidden'; ")
}
if (last != quale)
{
if ( quale++ < voci.length)
for (var i=quale ; i<voci.length ; i++ )
eval(beg+"['pr'+i]"+mid+".top=eval(top+alto*(voci[quale-1].length+i))");
quale--;
for (var i = 1 ; i < voci[quale].length ; i++)
eval(beg+"['sc'+"+quale+"+'_'+"+i+"]"+mid+".visibility='visible' ");
last=quale;
}
else
last=-1
}
Le prime due variabili
nn e
w3c costituiscono il browser-sniffer, saranno vere (
true) rispettivamente se il browser è Netscape 4 o se il browser supporta "getElementById", cioè se ci troviamo davanti a IE5 o superiore, Mozilla e derivati ed altri browser più recenti. Quando sono false tutte e due saremo in presenza di IE4.
In base a queste due variabili, vengono costruite altre due variabili (le prime due righe in rosso):
beg e
mid.
A cosa servano è presto detto, per eliminare una serie di
if () {} nella funzione di gestione ho fatto ricorso ad
eval() per far fare al browser il lavoro di identificazione della giusta sintassi da usarsi per identificare ed interagire con i livelli.
Dovremmo ormai sapere che per gestire i livelli bisogna usare:
- per Netscape 4
document.layers;
- per Internet Explorer 4
document.all;
- per IE5 e successivi e per Mozilla
document.getElementById.
Quest'ultimo valido anche per tutti i browser basati su
Gecko.
La funzione
DDMenuApri() si occupa principalmente di rendere visibili/invisibili i livelli delle voci del menu e di spostare i livelli delle voci principali per far posto alle secondarie.
Per un generico livello "pippo" creeremo:
- per Netscape 4 un tag
<layer name='pippo' ... >;
- per gli altri browser:
<div id='pippo' ... >
Posto che il posizionamento deilivelli sarà assoluto come in tutti gli altri menu, avremo dunque bisogno di queste sintassi per gestire i livelli con i diversi browser:
| Netscape 4 | Rendere visibile | document.layers.pippo.visibility='show' document.layers.pippo.visibility='visible' (*) |
| Rendere invisibile | document.layers.pippo.visibility='hide' document.layers.pippo.visibility='hidden' (*) |
Spostare a 20px dal margine superiore | document.layers.pippo.top=20 |
| |
| Internet Explorer 4 | Rendere visibile | document.all.pippo.style.visibility='visible' |
| Rendere invisibile | document.all.pippo.style.visibility='hidden' |
Spostare a 20px dal margine superiore | document.all.pippo.style.top=20 |
| |
| Altri browser | Rendere visibile | document.getElementById("pippo").style.visibility='visible' |
| Rendere invisibile | document.getElementById("pippo").style.visibility='hidden' |
Spostare a 20px dal margine superiore | document.getElementById("pippo").style.top=20 (**) |
Tornando alle due variabili:
var beg=nn?"document.layers":w3c?"document.getElementById(":"document.all";
var mid=nn?"":w3c?").style":".style";
notiamo che la "beg" (che ovviamente sta per "begin") assume, sotto forma di stringa, la parte iniziale della sintassi necessaria ad identificare il livello (le porzioni in blu nella tabella), mentre l'altra variabile "mid" (che sta per "middle") assume la parte intermedia (quando c'è, per Netscape 4 è assente) evidenziata in verde nella tabella.
Nella tabella è rimasto:
- in nero il nome del livello;
- in rosso la caratteristica (proprietà) vera e propria da modificare per il livello ed il valore da assegnare.
Supponendo che il browser sia Mozilla, "w3c" sarà
true, quindi avremo:
- beg="document.getElementById(";
- mid=").style"
e per rendere invisibile il livello, ricordando che il nome del livello (in questo caso) ed il valore da assegnare (sempre) vanno passati come stringhe se concateniamo la stringa
beg più la stringa "
'pippo' " più la stringa
mid più la stringa "
.visibility='hidden' " (
notare l'uso che ho fatto di " e di ' ) otterremo la stringa cercata:
| beg | 'pippo' | mid | .visibility='hidden' |
| document.getElementById( | 'pippo' | ).style | .visibility='hidden' |
quindi la stringa: "
document.getElementById('pippo').style.visibility='hidden' ".
Passando ad
eval(), che compare più volte nella funzione, sappiamo che accetta in ingresso, come argomento, una stringa che contenga una o può più qualsiasi istruzione JavaScript, la interpreta ed esegue (si comporta cioè come il browser che esegue uno script), quindi se passiamo ad eval() quella stringa ne otterremo l'esecuzione.
Con tre righe di JavaScript (le due variabili "beg" e "mid" e l'istruzione eval() con la stringa volta per volta costruita ad arte) ci siamo risparmiati una serie di
if () {} all'interno della funzione (ottenendone inoltre uno snellimento ed una maggiore leggibilità della stessa) ed abbiamo lasciato al browser il compito di scegliere da sè la sintassi di cui necessita per interagire con i livelli.
Insomma il browser fa il lavoro sporco, come qualsiasi macchina. Il che è esattamente ciò per cui le macchine sono nate a fare.
Commentare queste tre righe di codice non è stato facile, e spero di essere stato comprensibile, ma come si vede ne vale ampiamente la pena, basta confrontare una porzione della funzione DDMenuApri() (v. le due righe in verde) con una funzione (ad esempio la coloratutti() del menu a tendina) che anche agisce su n livelli per rendersi conto di quanto è più leggibile la DDMenuApri(): senza il giochetto di stringhe e l'uso di eval() sarebbe stato necessario scrivere una funzione, o un bel gruppo di righe di codice all'interno della DDMenuApri(), per ogni occorrenza di eval() nella funzione, cioè per ben quattro volte!!!
C'è un'altra chicca, sempre in questo menu e sempre nella parte evidenziata in verde, che voglio evidenziare: un eval() dentro l'altro!
Riprendiamo la riga:
eval(beg+"['pr'+i]"+mid+".top=eval(top+alto*(voci[quale-1].length+i)) ");
che si riferisce al posizionamento dei livelli principali (proprietà "top" dei livelli il cui
id inizia per "pr" [v. dopo]).
A sinistra del segno di "=" abbiamo la concatenazione delle stringhe che abbiamo appena visto,mentre a destra un altro eval() che determina la posizione verticale dell'i-esimo livello in base alla variabile "top" (distanza del menu dal margine superiore del browser), alla "alto" (altezza in px dei livelli) e numero di livelli principali fino a i-1.
L'interprete JavaScript esegue l'eval()
esterno ricavando la stringa che identifica il livello, durante l'esecuzione di questo si ritrova un altro eval() da eseguire per ricavare il valore (sempre una stringa!!! notate i doppi apici in rosso!!!) che ovviamente eseguirà
prima di terminare l'esecuzione di quello esterno
L'interprete ovviamente non fa una piega... noi abbiamo risparmiato almeno un'altra riga di codice ed un paio di { e }... e pensare che c'è chi pensa che JavaScript serva solo a validare i form! :-)
Tornando allo script (era ora!) ci eravamo lasciati su
var w3c= ... .
La riga successiva è una variabile di stato che servirà alla funzione DDMenuApri() per capire se c'è un menu principale aperto, subito dopo un if () che tramite document.write() scrive nel nostro documento HTML i necessari tag per i livelli usando LAYER per Netscape4 (porzione che ho riportato in questa scheda) e DIV per tutto il resto (v. il sorgente, qui ho lasciato i puntini di sospensione per non complicare le cose).
Il ciclo principale viene eseguito per la lunghezza dell'array
voci quindi per ogni voce principale.
Il primo document.write() scrive i livelli per le voci principali:
name='pr"+i+"' quindi scriverà nel documento "pr0", "pr1", "pr2" e "pr3" (nell'esempio ci sono 4 voci principali), subito dopo un altro ciclo for () conta e scrive gli elementi di ogni voce principale, quindi gli elementi secondari, e questa volta abbiamo
name='sc"+i+"_"+ii+"' ; ad esempio scriverà "sc0_0", "sc0_1, "sc0_2"... La prina cifra (0) viene dal contatore
i (ciclo esterno: livelli principali), la seconda dal contatore
ii (ciclo interno: livelli secondari), il carattere di underscore è necessario per evitare che i valori di i e di ii vengano sommati piuttosto che concatenati.
Alla fine dei cicli, ritroveremo scritto nel codice HTML del nostro documento un livello per ogni voce del menu (per gli altri browser invece di "name" si usa "id" ma la sostanza non cambia) ed ogni livello è univocamente determinato e individuabile dall'interprete.
Nei document.write() viene anche predefinita la posizione (proprietà "left" e "top"), ma non ci interessa discuterne, sono invece molto importanti quei due suffissi: "pr" e "sc" che ritroviamo in tutti gli eval() della funzione DDMenuApri().
È arrivato infatti (finalmente!) il momento di vedere come funziona.
Notiamo subito che accetta un parametro ("quale") che è l'identificativo del menu da aprire:
quale assumerà valori da
0 a
voci.length-1 individuando univocamente uno dei livelli principali, valore che viene scritto direttamente dai document.write().
La funzione inizia ciclando in tutti gli array e:
- riporta alla posizione originaria (
top+alto*i, v. il sorgente) i livelli principali tramite il primo eval();
- nasconde tutti i livelli secondari tramite il secondo eval().
Poi controlla se il livello cliccato (
quale) è diverso dall'ultimo aperto: abbiamo detto poco fa che la
last identifica l'ultimo livello che è stato aperto (ed in tal modo viene settata dalla funzione nella penultima assegnazione), altrimenti segnala che non c'è alcun livello aperto (ha valore -1 come da assegnazione iniziale o per assegnazione della stessa funzione nell'ultima riga: l'else dell'if() in oggetto.
Bene: se
last != quale allora si è cliccato: o su un menu diverso da quello aperto, oppure su un menu mentre non ce n'è nessun altro menu aperto. In ogni caso nell'istante in cui la if() viene eseguita
nessun menu è aperto (sono stati appena
chiusi tutti dal ciclo precedente) quindi il livello cliccato
è da aprire.
La funzione incrementa di 1 unità la variabile
quale (quale++) e controlla che
non corrisponda all'ultima voce principale. Se
non si tratta dell'ultima voce principale sposta in basso (cominciando da
quale che a questo punto è il livello succesivo grazie all'incremento) tutti i livelli successivi di quel tanto che basta ad aprire il livello cliccato: cioè di
voci[quale-1].length quindi della lunghezza dell'array cliccato: ergo del numero di sottovoci da visualizzare più uno (grazie al length che conta il numero di elementi dell'array). Ovviamente se invece si tratta dell'ultimo menu, niente dev'essere spostato, da qui il controllo.
Fatto questo
quale viene di nuovo decrementata (quale--) per lavorare sull'array di voci secondarie corrispondenti alla voce cliccata, ed il ciclo successivo si occupa di rendere visibili le relative voci.
Il menu a questo punto è aperto, alla
last viene assegnato il valore di
quale e la funzione termina.
Notate che abbiamo ottenuto, senza scrivere codice ad hoc, anche
la chiusura del menu.
Infatti la funzione inizia
sempre chiudendo tutto il menu, ed aprendo un livello solo se si è cliccato su un livello diverso dall'ultimo cliccato (il "last" appunto): per cui se in un dato istante si è cliccato (ad esempio) sul livello 2 mentre
last valeva
-1 o un altro valore diverso da 2, la funzione chiude tutto (primo ciclo) ed apre il livello 2 (perché
last è diverso da
quale). Al termine assegna a
last il valore 2.
Se clicchiamo di nuovo sul livello 2 la funzione inizia di nuovo chiudendo tutto, ma
non apre niente perché la condizione
last != quale non è verificata e, per l'esecuzione dell'
else, a
last viene assegnato di nuovo il valore -1 di partenza.
Il ciclo così ricomincia dalla situazione iniziale.
E questo è quanto.
Le funzioni ed il codice usato sembrerebbero poca cosa a prima vista (ho commentato a malapena 800 byte di codice), ma sembra inverosimile racchiudano invece tanta complessità e tanti automatismi.
Questo è il modo in cui mi piace codare JavaScritpt, spero che apprezzerete questo mio modo di fare e mi perdonerete, di conseguenza, per il tempo che faccio passare fra un aggiornamento e l'altro del sito.
Note:
(*)
Netscape 4: anche se "hide" e "show" sono i valori corretti da usare per settare la visibilità dei layers, "visible" e "hidden" possono comunque essere usati
quando lo stato viene variato via JavaScript. "hide" e "show" vanno obbligatoriamente usati solo se lo stato viene impostato in un foglio di stile.
(**)
per Mozilla e gli "altri" in generale: il valore per la posizione andrebbe espresso, a rigore, sotto forma di stringa e comprendendo l'unità di misura (ad esempio
...style.top='20px'). Il valore numerico e l'omissione dell'unità di misura non generano comunque errori ed il browser assume come "sottinteso" che il valore (numerico) passato sia espresso in pixel.