Prosty Lightbox

Działanie Lightboxa polega na wyświetlaniu na przyciemnionym tle obrazka, do którego prowadzi link-miniaturka. I takie właśnie działanie za implementujemy.
Poniżej zajmiemy się stworzeniem prostego obiektu, który będzie miał właśnie takie działanie. Standardowo najłatwiej i najlepiej użyć gotowych rozwiązań np. Lightbox 2. Poniższy tekst potraktuj raczej jako poznanie kilku technik lub powtórkę z tego co już znasz.

Zobacz DEMO naszego lightboxa

Założenia

Naszą pracę zaczynamy od długopisu i kartki papieru, na której wypiszemy kilka założeń:

  • Naszego lightboxa napiszemy obiektowo
  • Nasz lightbox będzie można wykorzystać kilka razy na stronie
  • Wszystko będzie tworzone dynamicznie
  • Opis zdjęcia pobieramy z atrybutu title
  • Niestety IE6 nas nie interesuje - chociaż jak ktoś chce, może się tym zająć (patrz koniec strony)

Nasze działania będą wyglądać następująco: Pobieramy link prowadzący do dużego zdjęcia. Po pobraniu linka, sprawdzamy, czy prowadzi on do grafiki (czyli sprawdzamy rozszerzenie). Jeżeli nie - niech się tym zajmą bardziej rozbudowane lightboxy - my wyświetlamy tylko grafiki. Jeżeli link prowadzi jednak do grafiki, wtedy po jej wczytaniu dynamicznie tworzymy tło zasłaniające stronę oraz blok zawierający div z opisem. Dodatkowo anulujemy domyślną akcję linka (przeniesienie). Po kliknięciu na tło lub przycisk zamykający, usuwamy z kodu strony tło, kontener ze zdjęciem i kończymy działanie. Tyle odnośnie teorii. Zaczynamy pracę.

Klasa podstawowa

Zaczynamy więc od utworzenia prostej klasy wyjściowej:


  _Lightbox = function(_obW) {
    this.obW = _obW;
    this.opis = _obW.title || "...";
    this.div_opis = null;
    this.div_background = null;
    this.div_container = null;
    this.image = null;
    this.imageWidth = 0;
    this.imageHeight = 0;
    this.loading = null;
    this.o = this;
    
    this.init = function() {
        ...
    }
    this.init();
}

Nasza klasa posiada kilka właściwości, oraz jedną metodę, która będzie ją inicjowała. obW wskazuje na obiekt (link), który utworzył naszego lightboxa. Dzięki temu możemy pobrać ścieżkę do interesującego nas pliku, oraz opis dla zdjęcia (atrybut title danego linku). Zauważ jak podstawiam pod opis tą wartość. To znany sposób. Jeżeli wartość istnieje i jest różna od "" (wiemy, że "" jest równe false), wtedy zostanie podstawiona, inaczej podstawiamy "...". Kolejne właściwości, których nazwa zaczyna się od div_ będą zawierać divy stanowiące kod naszego lightobxa. Do właściwości image załadujemy dużą grafikę.

działanie lightboxa

Właściwość loading będzie zawierać obrazek ładowania - omówimy ją za moment. Ostatnia właściwość wskazuje na sam obiekt - wykorzystamy tutaj znaną nam metodę odwoływania się do obiektów rodziców.

Metoda init()

Po kliknięciu na miniaturkę powinniśmy pokazać naszego lightboxa. Zasłanianie całej strony przed załadowaniem dużej grafiki nie jest jednak najlepszym rozwiązaniem. Super bohaterowie tak nie postępują. Dlatego przed przysłonięciem strony, poczekamy na całkowite załadowanie dużej grafiki. Do tego czasu pokażemy grafikę ładowania, którą wstawimy do wnętrza klikniętego linka.

    
this.init = function() {
    var ob = this.o;

    this.loading = document.createElement('span');
    this.loading.className = 'lightbox-loading';
    this.obW.appendChild(this.loading);

    var href = this.obW.href;

    this.image = new Image();
    this.image.onload = function () {
        ob.loading.parentNode.removeChild(ob.loading);
        ob.loading = null;

        ob.imageWidth = ob.image.width;
        ob.imageHeight = ob.image.height;

        //....
        //tworzymy lightboxa
        //....
    }
    this.image.src = this.obW.href;
}

Kliknij aby pokazać/ukryć grafikę ładowania


Tworzymy span z grafiką ładowania. Następnie wstawiamy go do klikniętego linka. Kolejnym krokiem jest utworzenie nowego obiektu typu Image, który jest naszą dużą grafiką. Po jej załadowaniu pobieramy wymiary, usuwamy grafikę ładowania oraz zaczynamy tworzyć naszego lightboxa. Właściwość src dla tego obiektu ustawiamy dopiero po zadeklarowaniu funkcji zdarzenia onload, inaczej IE wyrzuci błąd (więcej na ten temat możecie poczytać tutaj).

Wstrzymanie się z pokazywaniem lightboxa do czasu załadowania dużej grafiki ma dwa cele. Pierwszy już poznaliśmy - estetyczny. Drugi - techniczny - dotyczy dotyczy rozmiary naszego przyszłego kontenera. Jego wymiary będziemy dynamicznie ustawiać na podstawie wymiarów dużej grafiki. Przed jej załadowaniem nie jesteśmy w stanie pobrać jej wymiarów, co skutkuje błędnym ustawieniem rozmiarów białego kontenera. Po załadowaniu możemy już pobrać wymiary obrazka, więc to robimy. Zauważysz pewnie, że właściwości imageWidth i imageHeight nie są nam za bardzo potrzebne, bo wykorzystujemy je tylko w metodzie init() (równie dobrze mogły by być zmiennymi lokalnymi). Ja osobiście zrobiłem je jako właściwości obiektu, gdyż dzięki temu łatwiej będę mógł w przyszłości ten obiekt rozbudować.

Grafika załadowana, możemy więc przystąpić do tworzenia elementów naszego lightboxa: tła, kontenera, dużej grafiki, opisu i inputa zamykającego.

Na początku tworzymy blok z tłem, którym przykrywamy całą stronę:

this.init = function() {
    var body = document.getElementsByTagName('body')[0];

    ...
    
    this.image.onload = function () {
        ob.loading.parentNode.removeChild(ob.loading);
        ob.loading = null;

        ob.imageWidth = ob.image.width;
        ob.imageHeight = ob.image.height;

        ob.div_background = document.createElement('div');
        ob.div_background.className = 'lightbox-background';
        ob.div_background.style.width = window.innerWidth;
        ob.div_background.style.height = window.innerHeight;
        ob.div_background.title = "Kliknij aby zamknąć";
        ob.div_background.onclick = function() {
            ob.destroy();
        }
        body.appendChild(ob.div_background);    
    }        
}

Po kliknięciu na tło, będziemy chcieli usunąć naszego lightboxa, dlatego ustawiamy zdarzenie onclick dla tła, w którym wywołujemy metodę destroy (napiszemy ją za chwilę). Tło rozciągamy do rozmiarów całej powierzchni okna za pomocą właściwości innerWidth i innerHeight obiektu window. Pozycjonowanie fixed sprawia, że niezależnie od położenia strony, nasze tło okrywa całą widoczną powierzchnię. Tło dołączamy na końcu sekcji body, którą pobieramy za pomocą getElementsByTagName().

Kolejnym blokiem który utworzymy jest kontener w którym umieścimy dużą grafikę, przycisk zamykający (X) i opis grafiki. Taki blok musimy dołączyć tuż za blokiem z tłem. Pamiętaj by nie pomylić się i nie wstawiać go w blok tła, gdyż ten ma nałożoną przezroczystość (opacity), a nie chcemy by nasz kontener też ją miał.


this.init = function() {
    ...
    
    this.image.onload = function () {
        ...
        body.appendChild(ob.div_background);    
        ...
        
        ob.div_container = document.createElement('div');
        ob.div_container.className = 'lightbox-container';
        ob.div_container.style.marginTop = -(ob.imageHeight/2+20)+'px';
        ob.div_container.style.marginLeft = -(ob.imageWidth/2)+'px';
        ob.div_container.style.width = ob.imageWidth + 'px';
        ob.div_container.style.height = ob.imageHeight+20 + 'px';
        ob.div_container.appendChild(ob.image);    
    }
}

Naszemu kontenerowi ustawiamy odpowiednie wymiary i pozycję, które obliczamy na podstawie wymiarów pobranych z dużej grafiki. Wykorzystujemy do tego metodę centrowania za pomocą CSS, która ma ogólną postać:


    /* centrowanie dowolnego elementu w pionie i poziomie */
    position:absolute; //w naszym, przypadku fixed 
    left:50%; 
    top:50%; 
    margin-left: -połowa szerokości px;
    margin-top: -połowa wysokości px;

Po określeniu wymiarów kontenera, dołączamy do niego dużą grafikę (ob.image)

Pozostały nam 2 rzeczy do dołączenia do kontenera - opis grafiki oraz przycisk zamykający.

Opis grafiki tworzymy tak samo jak poprzednie elementy, wstawiając do niego tekstowego noda z opisem pobranym wcześniej z atrybutu title:


ob.div_opis = document.createElement('div');
ob.div_opis.className = 'opis';
ob.div_opis.appendChild(document.createTextNode(ob.opis));
ob.div_container.appendChild(ob.div_opis);

Kolejny raz powtarzamy nasze działanie by utworzyć przycisk zamykania. Wykorzystuje on do tego tą samą metodę z której korzysta tło naszego lightboxa. Po utworzeniu przycisku, dołączamy go do naszego kontenera.


var btnClose = document.createElement('input');
    btnClose.type = "button";
    btnClose.className = "close";
    btnClose.value = "x";
    btnClose.onclick = function() {
        ob.destroy();
    }
ob.div_container.appendChild(btnClose);

Cały kod naszej metody ma teraz postać:


this.init = function() {            
    var ob = this.o;
    var body = document.getElementsByTagName('body')[0];
    var href = this.obW.href;
    this.loading = document.createElement('span');
    this.loading.className = 'lightbox-loading';
    this.obW.appendChild(this.loading);

    this.image = new Image();
    this.image.onload = function () {
        ob.loading.parentNode.removeChild(ob.loading);
        ob.loading = null;

        ob.imageWidth = ob.image.width;
        ob.imageHeight = ob.image.height;

        ob.div_background = document.createElement('div');
        ob.div_background.className = 'lightbox-background';
        ob.div_background.style.width = window.innerWidth;
        ob.div_background.style.height = window.innerHeight;
        ob.div_background.title = "Kliknij aby zamknąć";
        ob.div_background.onclick = function() {
            ob.destroy();
        }
        body.appendChild(ob.div_background);

        ob.div_container = document.createElement('div');
        ob.div_container.className = 'lightbox-container';
        ob.div_container.style.marginTop = -(ob.imageHeight/2+20)+'px';
        ob.div_container.style.marginLeft = -(ob.imageWidth/2)+'px';
        ob.div_container.style.width = ob.imageWidth + 'px';
        ob.div_container.style.height = ob.imageHeight+20 + 'px';
        ob.div_container.appendChild(ob.image);

        ob.div_opis = document.createElement('div');
        ob.div_opis.className = 'opis';
        ob.div_opis.appendChild(document.createTextNode(ob.opis));
        ob.div_container.appendChild(ob.div_opis);
        
        var btnClose = document.createElement('input');
            btnClose.type = "button";
            btnClose.className = "close";
            btnClose.value = "x";
            btnClose.onclick = function() {
                ob.destroy();
            }
        ob.div_container.appendChild(btnClose);

        body.appendChild(ob.div_container);
    }
    this.image.src = this.obW.href;        
}
this.init();

Metoda destroy()

Po kliknięciu na tło, albo na przycisk zamykający powinniśmy usunąć nasz lightbox. Napiszmy więc stosowną metodę:


this.destroy = function() {
    this.div_background.parentNode.removeChild(this.div_background);
    this.div_container.parentNode.removeChild(this.div_container);
    delete this;
}

Usuwamy blok z tłem, blok z kontenerem, a na końcu cały obiekt lightboxa. Prosta czarna magia.

Metoda dla linków miniaturek

Tworzenie naszego obiektu lightbox mamy zakończone. Przyszedł czas by go uwolnić :).
Aby łatwo móc używać naszego nowego obiektu, rozszerzmy obiekty typu Node (czyli wszystkie elementy na stronie) o możliwość odpalenia naszego super lightboxa:


Node.prototype.lightbox = function() {
    if (!(new RegExp('^.+jpg|png|jpeg|gif$', 'gi')).test(this.href)) return
    this.onclick = function() {
        var lighbox = new _Lightbox(this);
        return false;
    }        
}

Naszego lightboxa wywołujemy tylko dla linków prowadzących do grafik, stąd za pomocą wyrażeń regularnych sprawdzamy atrybut href linka. Jeżeli kończy się standardowym rozszerzeniem dla grafiki (jpg, png, jpegm, gif) to ustawiamy dla niego zdarzenie onclick tworzące naszego lightboxa.

Od tej pory nasze dzieło możemy wywołać dla wszystkich linków - np z klasą "kartofel-lightbox":

var a = document.getElementsByTagName('a');
    for (i=0; i<a.length; i++) {
        if (a[i].className == 'kartofel-lightbox') {
            a[i].lightbox();
        }
    }

Stylowanie naszego lightboxa

Praca zakończona. ostatnim krokiem jest ostylowanie naszego dzieła.
Wpierw stylujemy linki miniaturki. Pamiętajmy by miały ustawioną position:relative gdyż korzystamy z tego przy wstawianiu grafiki ładowania do ich wnętrza (z wykorzystaniem wcześniej opisanego sposobu wyśrodkowania z pomocą css):

.kartofel-lightbox {
    margin:3px;
    padding:2px;
    border:1px solid #ccc;
    position:relative;
    display:block;
    width:130px;
    height:130px;
    float:left;
}

.kartofel-lightbox img {
    border:0;
}

Pozostało nam ostylowanie naszego lightboxa:


/* tło zasłaniające całą stronę */
.lightbox-background {
    position:fixed;
    left:0;
    top:0;
    width:100%;
    height:100%;
    overflow:hidden;
    background:#333;
    opacity:0.5;
    -moz-opacity:0.5;
    z-index:9999;
    cursor:pointer;
}

/* kontener z duża grafiką */ 
.lightbox-container {
    background:#fff;
    padding:10px;
    position:fixed;
    z-index:10000;
    left:50%;
    top:50%;
    -moz-border-radius:10px;
    -webkit-border-radius:10px;
    -moz-box-shadow:#333 0px 0px 7px;
    -webkit-box-shadow:#333 0px 0px 7px;
}

/* opis dużej grafiki */
.lightbox-container .opis {
    font:12px Arial;
    color:#666;
    text-align:center;
    border-top:1px solid #eee;
    padding-top:7px;
}

/* przycisk zamykania */
.lightbox-container .close {
    border:0; 
    width:28px;
    height:28px;
    position:absolute; 
    right:-7px;
    top:-7px;
    text-indent:-999px;
    cursor:pointer;
    background:url(lightbox-close.png) no-repeat;
}

/* grafika ładowania wstawiana do miniaturek */
.lightbox-loading {
    background:url(loading.gif) no-repeat;
    width:20px;
    height:20px;
    position:absolute;
    left:50%;
    top:50%;
    margin:-10px;
    overflow:hidden;
    -moz-border-radius:3px;
    -webkit-border-radius:3px;
}

Wszystko razem

Zbierzmy wszystko razem do kupy:

HTML:


<a href="kartofel_gears.jpg" title="Super bohater wojny z Szarańczą" class="kartofel-lightbox">
    <img src="kartofel_gears_thumb.jpg" width="130" height="130" alt="" />
</a>
<a href="wietnam.jpg" title="Witamy w wietnamie" class="kartofel-lightbox">
    <img src="wietnam_thumb.jpg" width="130" height="130" alt="" />
</a>

Javascript:


  _Lightbox = function(_obW) {
    this.obW = _obW;
    this.opis = _obW.title || "...";
    this.div_opis = null;
    this.div_background = null;
    this.div_container = null;
    this.image = null;
    this.imageWidth = 0;
    this.imageHeight = 0;
    this.loading = null;
    this.o = this;
    
    this.destroy = function() {
        this.div_background.parentNode.removeChild(this.div_background);
        this.div_container.parentNode.removeChild(this.div_container);
        delete this;
    }

    this.init = function() {            
        var ob = this.o;
        var body = document.getElementsByTagName('body')[0];
        var href = this.obW.href;
        this.loading = document.createElement('span');
        this.loading.className = 'lightbox-loading';
        this.obW.appendChild(this.loading);

        this.image = new Image();
        this.image.onload = function () {
            ob.loading.parentNode.removeChild(ob.loading);
            ob.loading = null;

            ob.imageWidth = ob.image.width;
            ob.imageHeight = ob.image.height;

            ob.div_background = document.createElement('div');
            ob.div_background.className = 'lightbox-background';
            ob.div_background.style.width = window.innerWidth;
            ob.div_background.style.height = window.innerHeight;
            ob.div_background.title = "Kliknij aby zamknąć";
            ob.div_background.onclick = function() {
                ob.destroy();
            }
            body.appendChild(ob.div_background);

            ob.div_container = document.createElement('div');
            ob.div_container.className = 'lightbox-container';
            ob.div_container.style.marginTop = -(ob.imageHeight/2+20)+'px';
            ob.div_container.style.marginLeft = -(ob.imageWidth/2)+'px';
            ob.div_container.style.width = ob.imageWidth + 'px';
            ob.div_container.style.height = ob.imageHeight+20 + 'px';
            ob.div_container.appendChild(ob.image);

            ob.div_opis = document.createElement('div');
            ob.div_opis.className = 'opis';
            ob.div_opis.appendChild(document.createTextNode(ob.opis));
            ob.div_container.appendChild(ob.div_opis);
            
            var btnClose = document.createElement('input');
                btnClose.type = "button";
                btnClose.className = "close";
                btnClose.value = "x";
                btnClose.onclick = function() {
                    ob.destroy();
                }
            ob.div_container.appendChild(btnClose);

            body.appendChild(ob.div_container);
        }
        this.image.src = this.obW.href;
    }
    this.init();
}

Node.prototype.lightbox = function() {
    if (!(new RegExp('^.+jpg|png|jpeg|gif$', 'gi')).test(this.href)) return
    this.onclick = function() {
        var lighbox = new _Lightbox(this);
        return false;
    }        
}

window.onload = function() {
    var a = document.getElementsByTagName('a');
        for (i=0; i<a.length; i++) {
            if (a[i].className == 'kartofel-lightbox') {
                a[i].lightbox();
            }
        }
}

Stylowanie CSS:


.kartofel-lightbox {
    margin:3px;
    padding:2px;
    border:1px solid #ccc;
    position:relative;
    display:block;
    width:130px;
    height:130px;
    float:left;
}

.kartofel-lightbox img {
    border:0;
}

.lightbox-background {
    position:fixed;
    left:0;
    top:0;
    overflow:hidden;
    background:#333;
    opacity:0.5;
    -moz-opacity:0.5;
    z-index:9999;
    cursor:pointer;
}

.lightbox-container {
    background:#fff;
    padding:10px;
    position:fixed;
    z-index:10000;
    left:50%;
    top:50%;
    -moz-border-radius:10px;
    -webkit-border-radius:10px;
    -moz-box-shadow:#333 0px 0px 7px;
    -webkit-box-shadow:#333 0px 0px 7px;
}

.lightbox-container .opis {
    font:12px Arial;
    color:#666;
    text-align:center;
    border-top:1px solid #eee;
    padding-top:7px;
}

.lightbox-container .close {
    border:0; 
    width:28px;
    height:28px;
    position:absolute; 
    right:-7px;
    top:-7px;
    text-indent:-999px;
    cursor:pointer;
    background:url(lightbox-close.png) no-repeat;
}

.lightbox-loading {
    background:url(loading.gif) no-repeat;
    width:20px;
    height:20px;
    position:absolute;
    left:50%;
    top:50%;
    margin:-10px;
    overflow:hidden;
    -moz-border-radius:3px;
    -webkit-border-radius:3px;

}

Demo

Nasz gotowy lightbox prezentuje się tak:

Ujarzmiamy potwora?

Brak obsługi IE6 to dla niektórych duży minus. Nie jest to czas i miejsce na podejmowanie kolejnej dyskusji o słuszności tego podejścia.

Z chęcią porzucił bym tą sekcję tego artykułu, jednak dzięki niej poznamy kilka interesujących super czarów.

Pierwszy z nich to wykrywanie przeglądarki IE. Możemy do tego skorzystać z "navigator.appName" (np tutaj jest przykład), ale istnieje ciekawsza metoda. Wystarczy wykorzystać komentarze warunkowe. Pewnie każdy z was je zna i ich zastosowanie do tworzenia oddzielnych arkuszy stylów dla np IE6. Ale pewnie nie każdy wie jak wykorzystać je do wykrywania przeglądarki.


var ie = (function() {
    var undef, v = 3, div = document.createElement('div');
    
    while {
        div.innerHTML = '',
        div.getElementsByTagName['i'][0];
    };
    
    return v > 4 ? v : undef;
    
}());

Tworzymy komentarz warunkowy, a w jego wnętrzu jakiś element. Następnie pobieramy ten element, i jeżeli udało nam się wykonać takie pobranie - wtedy wiemy, że komentarz działa - czyli mamy do czynienia z daną wersją przeglądarki.

Jak wiemy, przeglądarka IE6 nie obsługuje position:fixed, więc nasze tło nie będzie "przyklejone" do widocznego obszaru strony. Jeżeli chcemy to poprawić, będziemy musieli przykryć nim całą powierzchnię naszej strony. Wystarczy skorzystać z poniższej funkcji, która zwraca obiekt z szerokością i w wysokością całej strony. Zawiłość jej kodu wynika z różnego obliczania powierzchni strony przez przeglądarki (taki IE8 może pracować przecież także w trybie zgodności):


function wymiaryStrony() {
    var d = document;
    var ie = (document.all && !window.opera);
    var iebody = (d.compatMode && d.compatMode != 'BackCompat') ? d.documentElement : d.body;    
    
    var dimension = {
        width:ie ? iebody.scrollWidth : (d.clientWidth || self.innerWidth), 
        height:}
        height = ie ? Math.max(iebody.scrollHeight, iebody.clientHeight) : (d.documentElement.clientHeight || self.innerHeight)
    }

    return dimension;
}

Przykrycie całej strony tłem nie rozwiązuje jednak całego problemu. Przy przewijaniu strony, nasz biały kontener który będzie teraz miał position:absolute nie będzie się przesuwał. Podczas przewijania (onscroll) strony jego pozycję musimy dynamicznie ustawiać wykorzystując właściwość scrollTop dla IE lub pageXOffset dla innych przeglądarek :)
Dodatkowo dla przeglądarek IE będziemy musieli zmienić wywoływanie naszego skryptu, gdyż IE nie za bardzo sobie radzi z instrukcją Nodami.


function lightbox(ob) {
    if (!(new RegExp('^.+jpg|png|jpeg|gif$', 'gi')).test(ob.href)) return
    ob.onclick = function() {
        var lighbox = new _Lightbox(this);
        return false;
    }        
}

window.onload = function() {
    var a = document.getElementsByTagName('a');
        for (i=0; i<a.length; i++) {
            if (a[i].className == 'obr') {
                lightbox(a[i]);
            }
        }
}

Ah byłbym zapomniał. Dodatkowe stylowanie dotyczące przezroczystość. Zapewne użycie niestandardowych właściwości CSS filter:alpha(opacity=50) dla naszego tła będzie bardzo wskazane. Chyba że chcemy, by nasz lightbox był nieco hardcorowy - wtedy najlepiej nie ustawiać przezroczystości, w zamian ustawiając kolor tła np na #00FF00.

Podsumowanie

Zakończyliśmy pisanie prostego lightboxa. Oczywiście nasze dzieło nie jest idealne, a do takiego jeszcze bardzo mu daleko. Jednak nie taki był cel tego "artykułu".
Co możemy poprawić, a co rozwinąć?
Pierwsze co rzuca się w oczy, to brak obsługi przechodzenia między kolejnymi zdjęciami. Zastosowanie dodatkowej tablicy będzie wskazane. Kolejnym usprawnieniem mogło by być skalowanie dużych grafik, gdy te się nie mieszczą w oknie przeglądarki.

Zachęcam do przejrzenia kodów kilku popularnych skryptów tego typu i wyłapaniu ciekawych technik, o które łatwo możemy rozbudować powyższy skrypt.