In questo articolo mostreremo come creare una esperienza di navigazione orizzontale dei contenuti, miscelando la voglia di riprodurre una user experience simile a quella offerta dagli slideshow o dai tablet, alla necessità di caricare contenuti in modo tradizionale. In altre parole l'idea è di "sfogliare il sito" caricando una pagina nuova ad ogni transizione.
Affronteremo la questione in tre parti:
- nella prima creeremo la struttura HTML e CSS del nostro slideshow, ovvero del contenitore che utilizzeremo per navigare le nostre pagine
- nella seconda affronteremo il codice jQuery di base
- nella terza ed ultima parte estenderemo il nostro slideshow al mondo mobile aggiungendo un tipico effetto "swipe" sulle slide, ossia lo scorrimento laterale ottenuto tramite il movimento delle dita.
La struttura dello slideshow
La caratteristica principale dello slideshow quella di rappresentare un contenitore che si estende su tutta la pagina.
Per non lasciare l'utente disorientato, utilizziamo un menu di navigazione e associeremo ad ogni pagina del sito una voce del menu. La voce di menu corrispondente alla slide visualizzata sarà evidenziata con un differente colore sia per il testo che per lo sfondo.
Il nostro slideshow quindi si comporrà dei seguenti elementi:
- Il contenitore principale delle slide
- Le pagine (slide) con i contenuti
- I pulsanti "Avanti" ed "Indietro" posti ai lati
- Un menu di navigazione superiore
Il contenitore principale è centrato nella pagina e al suo interno le slide verranno disposte una accanto all'altra mediante l'interazione tra CSS e jQuery.
I pulsanti dello slideshow saranno posizionati in modo assoluto ai lati della pagina e centrati verticalmente. Vediamo ora la struttura HTML del nostro slideshow:
<div id="site"> <div id="nav"> <ul id="navigation"> <li><a href="#home" class="current" data-rel="0">Home</a></li> <li><a href="#articoli" data-rel="1">Articoli</a></li> <li><a href="#about" data-rel="2">About</a></li> </ul> </div> <div id="content"> <div id="content-wrapper"> <div class="page" id="home"><!-- prima slide --></div> <div class="page" id="articoli"><!-- seconda slide --></div> <div class="page" id="about"><!-- terza slide --></div> </div> <a href="#" id="previous">Prec.</a> <a href="#" id="next">Succ.</a> <img src="img/loading.gif" alt="" id="loader"/> </div> </div>
Abbiamo aggiunto anche un'immagine GIF che fungerà da immagine di preload ogni volta che viene selezionata una pagina. Le voci del menu di navigazione sono legate alle slide in due modi:
- Tramite l'ancora contenuta nell'attributo
href
che corrisponde all'ID della slide. - Tramite l'attributo custom HTML5
data-rel
che in questo caso corrisponde all'indice della slide.
Questo legame ci semplificherà la vita quando definiremo il codice jQuery.
In un caso reale di navigazione orizzontale tra le categorie di un sito, cose come la definizione dei contenuti o la presenza di elementi come il menu di navigazione possono subire profondi cambiamenti: i contenuti potrebbero essere presi in modo dinamico dal CMS e il menu di navigazione integrato direttamente nel contenuto ad esempio. In questo momento però ci interessa mostrare un comportamento di base e semplificare tutto il resto.
Codice CSS
Nel nostro layout ci sono sia elementi posizionati in modo statico (nel flusso del documento) che elementi posizionati in modo assoluto (fuori dal flusso del documento). A metà di questi due contesti di formattazione si pongono i contenuti (le slide), che verranno flottati all'interno del contenitore.
Se ogni slide è larga 800 pixel, tre slide richiedono 2400 pixel di spazio. Ovviamente nessuno schermo ha questa risoluzione, quindi si impiega una tecnica fondamentale in ogni slideshow: il contenitore più esterno delle slide (#content
) avrà la dichiarazione overflow:hidden
. Tale dichiarazione ci permette di mostrare solo 800 pixel per volta, sopprimendo la visualizzazione del contenuto in eccesso.
Ma il cuore di ogni slideshow sta nel contenitore più interno (#content-wrapper
), la cui larghezza totale verrà impostata sommando le larghezze di tutte le slide. Questo farà in modo che le slide si dispongano su un'unica riga, ma ha anche un altro effetto importante: impostando infatti position: relative
su questo contenitore, non solo gli permetteremo di scorrere da destra a sinistra, ma faremo anche in modo che ciascuna slide abbia un offset sinistro definito che potremo usare con jQuery per far muovere il contenitore secondo la distanza necessaria a mostrare ciascuna slide.
Partiamo dalle dichiarazioni più generali:
* { margin: 0; padding: 0; } html, body { width: 100%; height: 100%; min-height: 100%; } body { background: #fff; color: #000; font: 90% Arial, sans-serif; } h1 { font-size: 2em; font-family: Georgia, serif; font-weight: normal; color: #08c; margin-bottom: 0.3em; } p { margin-bottom: 1em; line-height: 1.4; }
Abbiamo resettato i margini e il padding di tutti gli elementi in modo da non dovere dichiararli ogni volta. Quindi abbiamo fatto in modo di avere la pagina a piena larghezza ed altezza usando le proprietà width
, height
e min-height
.
A questo punto possiamo passare a definire il contenitore più esterno:
#site { width: 100%; }
Quindi tocca al menu di navigazione superiore. Si tratta di un semplice menu orizzontale con le singole allineate tramite display: inline
:
#nav { height: 3em; background: #000 url(img/menu.gif) repeat-x 0 0; } #nav ul { width: 100%; list-style: none; height: 100%; text-align: center; line-height: 3; } #nav li { display: inline; margin-right: 1em; } #nav a { color: #f96; padding: 0.4em 0.6em; text-decoration: none; text-transform: uppercase; letter-spacing: 0.1em; } #nav a:hover { color: #fff; } #nav a.current { background: #f96; color: #fff; }
La classe .current
verrà assegnata dinamicamente da jQuery ogni volta che si seleziona la slide corrente.
Passiamo ora al contenitore più esterno delle slide:
#content { margin: 2em auto; width: 100%; max-width: 800px; overflow: hidden; }
Usando una larghezza in percentuale e una larghezza massima in pixel facciamo in modo che il contenitore si adatti alla finestra del browser. Se c'è spazio disponibile, allora la larghezza sarà di 800 pixel, altrimenti inferiore.
Quindi tocca al contenitore più interno:
#content-wrapper { position: relative; }
La larghezza e l'altezza massima di questo elemento verranno fissate dinamicamente da jQuery in base al numero di slide e all'altezza massima del contenuto presente in esse. Quindi definiamo gli stili per ciascuna slide:
div.page { margin: 0; float: left; width: 800px; position: relative; }
La larghezza di ciascuna slide deve essere uguale alla larghezza massima del contenitore più esterno. Ora non resta che posizionare in modo assoluto i due pulsanti e la GIF animata del preload:
#previous, #next { width: 40px; height: 40px; position: absolute; text-indent: -9999px; } #previous { left: 0; background: url(img/arr-left.png) no-repeat; } #next { right: 0; background: url(img/arr-right.png) no-repeat; } #loader { position: absolute; top: 50%; left: 50%; margin: -16px 0 0 -16px; display: none; }
Abbiamo detto che i due pulsanti devono essere centrati verticalmente, ma sui dispositivi mobili c'è un problema: la centratura viene persa quando si ruota il dispositivo. Deleghiamo quindi questo compito a jQuery che centrerà i due elementi intercettando il cambio di orientamento del dispositivo.
La nostra GIF è invece centrata sia orizzontalmente che verticalmente tramite una nota tecnica CSS: proprietà top
e left
su 50% e margine superiore e sinistro impostati su un valore negativo pari alla metà dell'altezza e della larghezza (la GIF è di 32x32 pixel).
L'immagine viene nascosta perchè a noi interessa che appaia per un certo periodo di tempo solo quando si seleziona una slide.
Il codice jQuery dello slideshow
Useremo un approccio object-oriented per il nostro codice. Vogliamo infatti scrivere del codice che possa essere riutilizzato in più contesti modificando solo alcune impostazioni di base.
Il nostro oggetto dovrà avere i seguenti componenti:
- Gli elementi HTML di riferimento.
- Dei metodi di utility.
- Un core che conterrà le azioni cruciali.
- Un metodo di inizializzazione.
Ecco quindi la struttura di base:
var Slides = { Elements: { // Elementi dello slideshow }, Utils: { // Metodi di utilità generale }, fn: { // Core }, init: function() { // Inizializzazione } }; $(function() { Slides.init(); });
Gli elementi
Definiamo gli elementi comuni come proprietà dell'oggetto Elements
:
Elements: { links: $('a', '#navigation'), // links menu di navigazione pages: $('div.page', '#content'), // slide previous: $('#previous'), // bottone "Indietro" next: $('#next'), // bottone "Avanti" loader: $('#loader'), // spinner GIF per il preload wrapper: $('#content-wrapper') // contenitore interno delle slide }
Attenzione a quando si utilizzano oggetti letterali. Se ad esempio scrivete:
var obj = { test: $('#test'), list: $('ul', this.test) // Errore! };
this
in questo caso essendo contenuto nell'oggetto jQuery()
punta a jQuery
e non all'oggetto obj
. Infatti l'interprete JavaScript proverà ad accedere alla proprietà test
dell'oggetto jQuery
che è ovviamente undefined
.
Questo invece è corretto:
var obj = { test: $('#test'), list: $('ul', obj.test) // Funziona };
I metodi di utilità
Il primo metodo di utility che definiremo è quello che controlla il movimento delle slide, il preload e l'evidenziazione del link corrente nel menu di navigazione superiore.
Il cuore dello slideshow un contatore (definito più avanti nell'oggetto core) che verrà incrementato o decrementato di 1 a seconda che venga premuto il bottone "Avanti" o "Indietro".
Ovviamente bisogna verificare ad ogni clic che l'indice del contatore non sia inferiore a 0 o superiore al numero massimo di slide contenute nello slideshow.
Il nostro metodo di utility si occupa di:
- modificare l'opacità della slide in vista
- mostrare l'immagine di preload
- attendere 1 secondo
- nascondere l'immagine di preload
- incrementare o decrementare di 1 il contatore per selezionare la slide successiva
- far scorrere il contenitore delle slide da destra verso sinistra usando l'offset sinistro della slide selezionata
- usare il contatore per selezionare il link corrente del menu di navigazione e assegnargli la classe CSS
current
rimuovendola al contempo da tutti gli
altri link.
Utils: { doSlide: function(params) { params = $.extend({ container: Slides.Elements.wrapper, a: Slides.Elements.links, spinner: Slides.Elements.loader, pages: Slides.Elements.pages, button: 'next' }, params); params.pages.eq(Slides.fn.index). animate( { opacity: 0.5 }, 600, function() { params.spinner.show(); setTimeout(function() { params.spinner.hide(); if (params.button == 'next') { Slides.fn.index++; } else { Slides.fn.index--; } params.container.animate( { left: -params.pages.eq(Slides.fn.index).position().left }, 1000, 'linear', function() { params.a.eq(Slides.fn.index).addClass('current'). parents('ul').find('a').not(params.a.eq(Slides.fn.index)). removeClass('current'); } ); }, 1000); } ); } }
Lavoriamo con il metodo .eq()
a cui passiamo l'indice generato dal contatore, che ovviamente verrà incrementato o decrementato a seconda se si clicca sul bottone "Avanti" o "Indietro".
Il secondo metodo da definire è quello che centra verticalmente i bottoni "Avanti" e "Indietro". Si tratta di dividere per due l'altezza della finestra del browser e sottrarre al valore ottenuto la metà dell'altezza dei bottoni:
positionButtons: function() { var prev = Slides.Elements.previous; var next = Slides.Elements.next; var totalHeight = $(window).height(); prev.css({ top: (totalHeight / 2) - (prev.height() / 2) }); next.css({ top: (totalHeight / 2) - (next.height() / 2) }); }
Il core
Il metodo core contiene i componenti fondamentali dello slideshow, come l'indice incrementale e i metodi per lo sliding. Il primo metodo che andremo a definire, insieme al contatore, serve a dare le dimensioni al contenitore interno delle slide:
fn: { index: 0, setWidths: function() { var totalHeight = Math.max(Slides.Elements.pages.outerHeight()); var totalWidth = Slides.Elements.pages.eq(0).width() * Slides.Elements.pages.length; $('#content').css({ height: totalHeight }); Slides.Elements.wrapper.css({ width: totalWidth, height: totalHeight }); } }
Usiamo Math.max()
per calcolare l'altezza massima delle slide. Quindi per ottenere la larghezza complessiva moltiplichiamo la larghezza di una slide per il numero totale di slde. Quindi impostiamo l'altezza e la larghezza del contenitore interno su questi valori.
A questo punto associamo due metodi ai bottoni "Avanti" e "Indietro":
nextBtn: function() { var btn = Slides.Elements.next; var slides = Slides.Elements.pages; btn.click(function(evt) { evt.preventDefault(); slides.css('opacity', 1); if (Slides.fn.index >= 0) { if (Slides.fn.index == (slides.length - 1)) { Slides.fn.index = slides.length - 1; Slides.Elements.previous.click(); return; } Slides.Utils.doSlide(); } }); }, prevBtn: function() { var btn = Slides.Elements.previous; var slides = Slides.Elements.pages; btn.click(function(evt) { evt.preventDefault(); slides.css('opacity', 1); if (Slides.fn.index >= 0) { if (Slides.fn.index < 0 || Slides.fn.index == 0) { Slides.fn.index = 0; Slides.Elements.next.click(); return; } Slides.Utils.doSlide({ button: 'previous' }); } }); }
Ciascun metodo prima reimposta la corretta opacità su tutte le slide, quindi verifica che l'indice non sia inferiore a 0 o superiore al numero di slide. Se ciò dovesse accadere, ciascun metodo resetta l'indice e invoca il metodo opposto. Quindi viene utilizzato il metodo doSlide()
visto in precedenza.
L'ultimo metodo dell'oggetto core gestisce il menu di navigazione superiore mostrando la slide collegata a ciascun link:
navigationMenu: function() { var links = Slides.Elements.links; var wrapper = Slides.Elements.wrapper; var slides = Slides.Elements.pages; links.each(function() { var $a = $(this); var slide = $($a.attr('href')); var $index = $a.attr('data-rel'); $a.click(function(evt) { slides.css('opacity', 1); evt.preventDefault(); Slides.fn.index = $index; wrapper.animate({ left: -slide.position().left }, 1000, 'linear', function() { $a.addClass('current').parents('ul'). find('a').not($a).removeClass('current'); }); }); }); } }
Questo metodo fa una cosa importante:
var $index = $a.attr('data-rel'); Slides.fn.index = $index;
Utilizzando l'indice di ciascun link memorizzato nell'attributo data-rel
e impostandolo sull'indice dello slideshow si fa in modo che quando l'utente clicca sul link del menu e poi torna a cliccare sui bottoni, lo scorrimento riparta dalla slide collegata al link, senza perdere la sequenza.
Inizializzazione
Caricare tutti i metodi dell'oggetto fn
è qualcosa che può essere automatizzato usando un loop for...in
e lanciando solo quei membri che sono funzioni:
init: function() { for (var property in this.fn) { if (typeof this.fn[property] === 'function') { this.fn[property](); } } }
Centrare verticalmente i bottoni sui dispositivi mobile
Come abbiamo detto, la centratura verticale dei bottoni si perde sui dispositivi mobile quando l'utente ruota il dispositivo. Per questa ragione usiamo l'evento orientationchange
dell'oggetto window
associandogli il nostro metodo di utility positionButtons()
:
$(function() { Slides.init(); if (window.orientation) // Siamo su mobile? { $(window).bind('orientationchange', function() { Slides.Utils.positionButtons(); }); } });
Potete intanto visionare il nostro esempio in questa pagina.
Lo swipe
Esaminiamo ora come aggiungere un effetto swipe al nostro slideshow per i dispositivi mobile. Lo swipe non esiste come evento DOM sui dispositivi mobile, ma è la combinazione degli eventi touchstart
, touchmove
e touchend
. Il movimento è quello che si effettua con un dito verso destra o sinistra per sfogliare o far scorrere le pagine.
Implementare questo effetto da zero è molto difficile, ma fortunatamente esistono plugin jQuery come touchSwipe che riescono nell'intento. Useremo questo plugin per il nostro slideshow.
Aggiungere lo swipe
Useremo le due action di Touchswipe chiamate swipeLeft
e swipeRight
per intercettare lo swipe sull'intero blocco contenitore (ossia #site
). Alla prima action assoceremo l'azione registrata sul bottone "Indietro" e alla seconda quella sul bottone "Avanti".
Per farlo aggiungeremo un altro metodo di utility all'oggetto Utils
:
swiping: function() { $('#site').swipe({ swipeLeft: function(event, direction, distance, duration, fingerCount) { Slides.Elements.previous.click(); }, swipeRight: function(event, direction, distance, duration, fingerCount) { Slides.Elements.next.click(); } }); }
Quindi invochiamo questo metodo se siamo su mobile in fase di inizializzazione:
$(function() { Slides.init(); if (window.orientation) // Siamo su mobile? { $(window).bind('orientationchange', function() { Slides.Utils.positionButtons(); }); Slides.Utils.swiping(); } });
Potete vedere il nostro esempio completo in questa pagina.
L'esempio completo è allegato all'articolo.
Nelle parti precedenti abbiamo analizzato il codice jQuery necessario per implementare un effetto swipe sui dispositivi mobile. In questa parte vedremo come aggiungere AJAX al nostro slideshow e realizzare finalmente la navigazione orizzontale delle pagine.
La nuova struttura HTML
Anzitutto dobbiamo modificare la struttura del markup HTML eliminando i contenuti di tutte le slide eccetto la prima (che potrebbe essere la nostra home page):
<div class="page" id="home">
<h1>Home</h1>
<!-- continua -->
</div>
<div class="page" id="articoli"></div>
<div class="page" id="about"></div>
I contenuti saranno infatti recuperati via AJAX con una richiesta GET. Si possono immaginare diversi modi per sfogliare l'elenco delle pagine, noi sfrutteremo una semplice query string del tipo page=numero
.
Lo script lato server
In un ambiente di produzione il nostro script PHP userebbe il numero di pagina passato tramite AJAX per reperire i contenuti dal database. Nel nostro esempio ci limiteremo ad usare un array associativo per simulare questa interazione:
<?php
header('Content-Type: text/html');
$page = $_GET['page'];
if(ctype_digit($page) && strlen($page) == 1) {
$pageNum = $page;
} else {
exit();
}
$htmlContents = array(
'...', // primo contenuto
'...', // secondo contenuto
'...' // terzo contenuto
);
echo $htmlContents[$pageNum];
Lo script imposta subito il tipo di contenuto su text/html
. Meglio impostare il content type in modo esplicito per evitare che il server lo imposti in modo arbitrario.
C'è una elementare validazione dei dati: se il parametro GET page
è un numero e la sua lunghezza è di 1 carattere, allora viene impostata la variabile $pageNum
. Altrimenti lo script si interrompe. A questo punto occorrerebbe restituire un errore.
Alla fine ritorniamo il contenuto relativo al numero di pagina scelto e associato alla posizione nell'array.
Il codice jQuery
Dobbiamo modificare il metodo doSlide()
inserendo una chiamata al metodo $.get
prima che la pagina scorra:
Utils: {
doSlide: function(params) {
params = $.extend({
container: Slides.Elements.wrapper,
a: Slides.Elements.links,
spinner: Slides.Elements.loader,
pages: Slides.Elements.pages,
button: 'next'
}, params);
params.pages.eq(Slides.fn.index).
animate({
opacity: 0.5
}, 600, function() {
params.spinner.show();
setTimeout(function() {
params.spinner.hide();
if (params.button == 'next') {
Slides.fn.index++;
} else {
Slides.fn.index--;
}
$.get('ajax.php', {page: Slides.fn.index}, function(html) {
params.pages.eq(Slides.fn.index).html(html);
});
params.container.animate({
left: -params.pages.eq(Slides.fn.index).position().left
}, 1000, 'linear', function() {
params.a.eq(Slides.fn.index).addClass('current').
parents('ul').find('a').not(params.a.eq(Slides.fn.index)).
removeClass('current');
});
}, 1000);
});
},
//...
}
Ecco la nostra modifica:
$.get('ajax.php', {page: Slides.fn.index}, function(html) { params.pages.eq(Slides.fn.index).html(html); });
La query string GET viene passata come oggetto letterale che jQuery convertirà nel canonico page=numero
. In questo caso il numero di pagina corrisponde all'indice corrente della slide. Quindi la successiva funzione anonima riceve come parametro la stringa HTML restituita dal server con cui andiamo a creare il contenuto della slide.
Ora non ci resta che modificare il codice per il menu di navigazione superiore:
navigationMenu: function() { var links = Slides.Elements.links; var wrapper = Slides.Elements.wrapper; var slides = Slides.Elements.pages; links.each(function() { var $a = $(this); var slide = $($a.attr('href')); var $index = $a.attr('data-rel'); $a.click(function(evt) { slides.css('opacity', 1); evt.preventDefault(); Slides.fn.index = $index; $.get('ajax.php', {page: $index}, function(html) { slide.html(html); }); wrapper.animate({ left: -slide.position().left }, 1000, 'linear', function() { $a.addClass('current').parents('ul'). find('a').not($a).removeClass('current'); }); }); }); }
Il codice è pressoché identico a quello visto in precedenza. La differenza sta nel fatto che la slide di riferimento qui viene presa dall'attributo href
del link corrente che, come sappiamo, contiene un'ancora che punta alla slide.
Potete vedere l'esempio completo in questa pagina e il codice completo è nell'allegato.