Lightbox

W poniższym tekście zajmiemy się stworzeniem Lightboxa, czyli mechanizmu dość często stosowanego do pokazywania zdjęć na stronach.

Takich rozwiązań jest na necie dziesiątki, a najpopularniejszymi są Lightbox 2 czy Fancybox. W realnych projektach sięgał bym raczej po nie (czy po inne pasujące). Wynika to z faktu, że są one przetestowane przez setki użytkowników, a i dodanie ich do projektu to zaledwie kilka sekund.

Nie znaczy to jednak, że nie warto nauczyć się tworzyć coś "customowego", a przy okazji nauczyć czegoś nowego.

HTML i CSS

Robiąc na stronie galerie zdjęć, które po kliknięciu powinny pokazywać duże zdjęcia powinniśmy ją zrobić w formie serii linków zawierających miniaturki. Dzięki temu nawet jeżeli użytkownikowi nie działał Javascript, spokojnie będzie mógł przejść do podglądu dużego zdjęcia.


    <a class="gallery-el" href="big1.png" data-text="lorem ipsum sit dolor">
        <img src="mini1.png" alt="">
    </a>
    <a class="gallery-el" href="big2.png" data-text="lorem ipsum sit dolor">
        <img src="mini2.png" alt="">
    </a>
    <a class="gallery-el" href="big3.png" data-text="lorem ipsum sit dolor">
        <img src="mini3.png" alt="">
    </a>

Poniżej możesz znaleźć całe stylowanie i html dla naszego lightboxa, a demo znajdziesz tutaj.

Stwórz sobie html z powyższą galerią, dodaj plik z poniższymi stylami i wrzuć na chwilę poniższy kod lightboxa byś miał pewność, że z wyglądem nie będzie problemu.

Pokaż HTML

<div class="lightbox">
    <div class="lightbox-box">
        <div class="lightbox-count">
            1/5
        </div>
        <div class="lightbox-img-cnt">
            <button class="lightbox-prev">
                Poprzednie zdjęcie
            </button>
            <button class="lightbox-next">
                Następne zdjęcie
            </button>
            <img src="https://pokaimg.me/600x600" class="lightbox-img" alt="">
        </div>
        <div class="lightbox-text">
            Lorem ipsum dolor sit.
        </div>
        <button class="lightbox-close">
            Zamknij
        </button>
    </div>
</div>

Pokaż CSS

@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;800&family=Source+Sans+Pro:wght@300;400&display=swap');

.lightbox {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 100;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    justify-content: center;
    align-items: center;
    backdrop-filter: blur(1px);
    font-family: Poppins, sans-serif;
    padding: 20px;
    box-sizing: border-box;
}

.lightbox-box {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    align-self: center;
    justify-self: center;
    max-height: 100%;
    border: 1px solid red;
}

.lightbox-img-cnt {
    position: relative;
    background: #FFF;
    min-height: 200px;
    min-width: 200px;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
    border-radius: 3px;
    box-shadow: 2.8px 2.8px 2.2px rgba(0, 0, 0, 0.02),
    6.7px 6.7px 5.3px rgba(0, 0, 0, 0.028),
    12.5px 12.5px 10px rgba(0, 0, 0, 0.035),
    22.3px 22.3px 17.9px rgba(0, 0, 0, 0.042),
    41.8px 41.8px 33.4px rgba(0, 0, 0, 0.05),
    100px 100px 80px rgba(0, 0, 0, 0.07);
}

.lightbox-img-loading {
    width: 20px;
    height: 20px;
    border: 2px solid rgba(0, 0, 0, 0.3);
    border-right-color: #000;
    border-radius: 50%;
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    transform: translate(-50%, -50%) rotate(0);
    animation: lightboxLoadingAnim 0.5s 0s infinite linear;
}

@keyframes lightboxLoadingAnim {
    100% {
        transform: translate(-50%, -50%) rotate(1turn);
    }
}

.lightbox-img {
    max-width: 100%;
    height: 100%;
    max-height: 90vh;
    display: block;
}

.lightbox-prev,
.lightbox-next {
    text-indent: -9999px;
    overflow: hidden;
    position: absolute;
    left: 0;
    top: 0;
    width: 50%;
    height: 100%;
    background: none;
    border: 0;
    cursor: pointer;
    background-size: 35px;
    background-repeat: no-repeat;
    opacity: 0;
    transition: 0.3s opacity;
    outline: none;
}

@media (any-pointer: coarse) {
    .lightbox-prev,
    .lightbox-next {
        opacity: 1;
    }
}

.lightbox-prev {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fff' class='bi bi-chevron-left' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'%3E%3C/path%3E%3C/svg%3E");
    background-position: left 15px center;
}

.lightbox-next {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23fff' class='bi bi-chevron-right' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'%3E%3C/path%3E%3C/svg%3E");
    background-position: right 15px center;
    left: auto;
    right: 0;
}

.lightbox-prev:hover,
.lightbox-next:hover {
    opacity: 1;
}

.lightbox-close {
    position: absolute;
    right: 0;
    top: 0;
    width: 60px;
    height: 60px;
    text-indent: -999px;
    overflow: hidden;
    background: none;
    background: rgba(0,0,0,0.4);
    border: 0;
    cursor: pointer;
    transition: 0.3s opacity;
}

.lightbox-close::before,
.lightbox-close::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 30px;
    height: 2px;
    border-radius: 4px;
    background: #FFF;
    transform: translate(-50%, -50%) rotate(45deg);
}

.lightbox-close::after {
    transform: translate(-50%, -50%) rotate(-45deg);
}

.lightbox-text {
    text-align: center;
    color: #FFF;
    padding: 5px;
    font-weight: 400;
    font-size: 14px;
}

.lightbox-count {
    text-align: center;
    color: #FFF;
    padding: 5px;
    font-size: 13px;
}

Zaczynamy

Tworząc Slider wykorzystaliśmy składnię Class. Język Javascript jest na tyle plastyczny, że dane problemy możemy rozwiązywać na różne sposoby. Zamiast klasy i zabawy obiektami, nasz lightbox napiszemy za pomocą samych funkcji. Dzięki temu nie użyjemy ani razu this, co dla niektórych będzie zbawieniem.


function lightbox() {
    let DOM = getElementsReference(); //to zaraz dodamy

    return {} //tu będziemy wystawiać rzeczy, do których będzie dostęp z zewnątrz
}

Wszystkie elementy z jakich będzie składać się lightbox będziemy trzymać w obiekcie DOM. Dzięki temu o wiele łatwiej będzie nam się do nich odwoływać (szczególnie jak masz porządny edytor, który będzie ci podpowiadał), ale i nie pomylimy zmiennych przetrzymujących elementy na stronie z innymi zmiennymi, które np. trzymają aktualny licznik. Moglibyśmy też po prostu użyć odpowiednich przedrostków np. elLightbox, elButton, ale jakoś tak to brzmi po Meksykańsku...

Lightbox będzie składał się z kilku elementów, które za chwilę obsłużymy. Musimy więc mieć do nich referencję. Możemy je tworzyć za pomocą odpowiednich metod (np. createElement()), ale możemy też wrzucić cały kod jako innerHTML, a następnie wybrać odpowiednie elementy za pomocą querySelector(). Dzięki temu potencjalne poprawki będą o wiele łatwiejsze.

Stwórzmy więc dwie metody - jedna zwróci kod całego lightboxa, a druga wybierze odpowiednie elementy:


function lightbox() {
    let DOM = getElementsReference();

    function createLightbox() {
        const lightbox = document.createElement("div");
        lightbox.classList.add("lightbox");
        lightbox.innerHTML = `
            <div class="lightbox-box">
                <div class="lightbox-count">
                </div>
                <div class="lightbox-img-cnt">
                    <button class="lightbox-prev">
                        ${options.prevText}
                    </button>
                    <button class="lightbox-next">
                        ${options.nextText}
                    </button>
                    <img src="" class="lightbox-img" alt="">
                </div>
                <div class="lightbox-text">
                </div>
                <button class="lightbox-close">
                    ${options.closeText}
                </button>
            </div>
        `;
        return lightbox;
    }

    function getElementsReference() {
        const ob = {};
        ob.lightbox = createLightbox();
        ob.count = ob.lightbox.querySelector(".lightbox-count");
        ob.prev = ob.lightbox.querySelector(".lightbox-prev");
        ob.next = ob.lightbox.querySelector(".lightbox-next");
        ob.imgCnt = ob.lightbox.querySelector(".lightbox-img-cnt");
        ob.img = ob.lightbox.querySelector(".lightbox-img");
        ob.text = ob.lightbox.querySelector(".lightbox-text");
        ob.close = ob.lightbox.querySelector(".lightbox-close");
        return ob;
    }

    return {}
}

Do pobranych elementów podepnijmy odpowiednie zdarzenia:


function lightbox() {
    let DOM = getElementsReference();
    bindEvents();

    function createLightbox() {
        ...
    }

    function getElementsReference() {
        ...
    }

    function bindEvents() {
        DOM.prev.addEventListener("click", () => {
            prevImage();
        });

        DOM.next.addEventListener("click", () => {
            nextImage();
        });

        DOM.close.addEventListener("click", () => {
            hideLightbox();
        });

        //dodatkowo obsłużmy klawisze
        document.addEventListener("keyup", e => {
            if (e.key === "ArrowLeft") prevImage();
            if (e.key === "ArrowRight") nextImage();
            if (e.key === "Escape") hideLightbox();
        })
    }

    return {}
}

Doszły nam dodatkowe funkcje: prevImage(), nextImage() i hideLightbox(). Dodajmy je do naszego kodu:


function lightbox() {
    ...

    function showLightbox() {
        document.body.append(DOM.lightbox);
    }

    function hideLightbox() {
        DOM.lightbox.remove();
    }

    return {}
}

Metody prevImage() i nextImage() działają bardzo podobnie do tych, które pisaliśmy w Sliderze. Różnią się w zasadzie tym, że dodatkowo pobieramy z danego elementu (linka który kieruje do dużej grafiki) tekst, który trzymany jest w data-text (patrz pierwszy listing):


function lightbox() {
    let currentIndex = 0; //zmienna oznaczająca aktualnie wyświetlane zdjęcie

    ...

    function nextImage() {
        currentIndex++;
        if (currentIndex > links.length - 1) currentIndex = 0;
        //pobieramy adres na który kieruje dany link (na duże zdjęcie)
        const href = links[currentIndex].href;
        //pobieramy jego data-text który będzie opisem zdjęcia
        const text = links[currentIndex].getAttribute("data-text") ? links[currentIndex].getAttribute("data-text") : " "
        //pokazujemy zdjęcie
        showImage(href, text);
    }

    function prevImage() {
        currentIndex--;
        if (currentIndex < 0) currentIndex = links.length - 1;
        const href = links[currentIndex].href;
        const text = links[currentIndex].getAttribute("data-text") ? links[currentIndex].getAttribute("data-text") : " "
        showImage(href, text);
    }

    return {}
}

function lightbox() {
    ...

    function showImage(src, text = "") {
        DOM.img.src = src;
        DOM.text.innerHTML = text;
        showCountText();
    }

    return {}
}

Funkcja showCount() będzie służyć do pokazywania tekstu w elemencie count:


function lightbox() {
    ...

    function showCount() {
        DOM.count.innerHTML = `${currentIndex + 1} / ${links.length}`;
        DOM.count.style.display = "block";
    }

    return {
        showCount //to warto wystawić
    }
}

Dodawanie i usuwanie zdjęć

Zmieniając aktualnie wyświetlane zdjęcie, zmieniamy currentIndex w tablicy links. Napiszmy dwie funkcje, które będą służyły do dodawania i usuwania do niej linków prowadzących do dużych zdjęć:


function lightbox() {
    ...

    function addLinks(selector) {
        const elements = document.querySelectorAll(selector);

        elements.forEach(el => {
            if (el.tagName === "A" && el.href) {
                el.addEventListener("click", e => {
                    e.preventDefault();
                    const href = el.href;
                    const text = el.getAttribute("data-text") ? el.getAttribute("data-text") : " "
                    currentIndex = getCurrentIndex(el);
                    showLightbox();
                    showImage(href, text)
                })
                links.push(el);
            }
        })
    }

    return {
        showCount,
        addLinks
    }
}

Funkcja przyjmie jakiś selektor, który będzie służył do pobrania elementów (linków prowadzących do dużych zdjęć) ze strony. Takich elementów może być naście (w końcu to galeria). Robimy więc po nich pętlę, sprawdzając czy dane elementy są linkami i mają atrybut href (inaczej nie ma sensu ich dodawać). Ewentualnie moglibyśmy tutaj jeszcze sprawdzić czy rzeczywiście prowadzą do grafik:


const ext = el.href.slice(el.href.lastIndexOf(".") + 1);
const graphicExt = ["jpg", "jpeg", "gif", "svg", "png", "avif", "webp", "bmp"];
if (el.tagName === "A" && el.href && graphicExt.includes(ext)) {
    ...
}

Jeżeli to jest link i gdzieś kieruje, pobieram jego href i text. Link taki zostanie dodany to tablicy linków, po której będziemy się przemieszczać w funkcjach prevImage() i nextImage(). Gdy użytkownik kliknie na taką miniaturkę na stronie, my pokażemy mu lightboxa, ale też musimy wiedzieć, które zdjęcie właśnie wyświetlamy z tablicy linków (chociażby do tego by pokazać tą informację na liczniku i wiedzieć kiedy przeskoczyć do początkowego zdjęcia). Posłuży do tego funkcja getCurrentIndex(), którą zapiszemy za chwilę.


function lightbox() {
    ...

    function getCurrentIndex(img) {
        const index = [...links].indexOf(img);
        return index !== 0 ? index : 0;
    }

    ...
}

Po wykryciu indeksu pokazujemy lightboxa i wyświetlamy zdjęcie.


function lightbox() {
    ...

    function showLightbox() {
        document.body.append(DOM.lightbox);
    }

    ...
}

Kolejna w kolejce będzie funkcja usuwająca zdjęcia z tablicy links. Robimy więc pętlę po przekazanych do usunięcia elementach, sprawdzamy czy dany element jest linkiem, a następnie odfiltrowujemy tablicę links z pominięciem danego elementu:


function lightbox() {
    ...

    function removeLinks(selector) {
        const links = document.querySelectorAll(selector);

        links.forEach(el => {
            if (el.tagName === "A") {
                links = links.filter(linkToImg => el !== linkToImg);
            }
        });
    }

    return {
        showCount,
        addLinks,
        removeLinks
    }
}

Programista może wywołać funkcję addLinks() przekazując do niej tylko jeden element. Podobnie po usunięciu elementów za pomocą removeLinks() może zostać w tablicy tylko jeden (albo nic) elementów. W takim przypadku pokazywanie licznika i przycisków poprzedni/następny mija się z celem. Poprawmy więc powyższe funkcje:


function lightbox() {
    ...

    function addLinks(selector) {
        ...

        if (links.length > 1) {
            showCount();
            showPrevNext();
        } else {
            hideCount();
            hidePrevNext();
        }
    }

    function removeLinks(selector) {
        ...

        if (links.length > 1) {
            showCount();
            showPrevNext();
        } else {
            hideCount();
            hidePrevNext();
        }
    }

    ...
}

I dodajmy brakujące funkcje:


function lightbox() {
    ...

    function hidePrevNext() {
        DOM.prev.style.display = "none";
        DOM.next.style.display = "none";
    }

    function showPrevNext() {
        DOM.prev.style.display = "block";
        DOM.next.style.display = "block";
    }

    function hideText() {
        DOM.text.style.display = "none";
    }

    function showText() {
        DOM.text.style.display = "block";
    }

    function hideCount() {
        DOM.count.innerHTML = "";
        DOM.count.style.display = "none";
    }

    function showCount() {
        ...
    }

    return {
        showCount,
        hideCount,
        addLinks,
        removeLinks,
        hidePrevNext,
        showPrevNext,
        hideText,
        showText,
    }
}

Obsługa wczytywania grafiki

Gdy wywołujemy funkcję showImage() ustawiamy dla zdjęcia po prostu nowe src. Zdjęcie może się wczytywać jakiś czas, dlatego fajnie by było, gdyby użytkownik zobaczył jakąś mini ikonkę wczytywania (takie mikro interakcje są bardzo ważne).

Dodajmy dwie funkcje, które będą tworzyć i usuwać ikonkę wczytywania:


function lightbox() {
    ...

    function showLoading() {
        const div = document.createElement("div");
        div.classList.add("lightbox-img-loading");
        DOM.imgCnt.append(div);
    }

    function hideLoading() {
        if (DOM.imgCnt.querySelector(".lightbox-img-loading")) {
            DOM.imgCnt.querySelector(".lightbox-img-loading").remove();
        }
    }

    ...
}

A następnie podepnijmy je dla zdjęcia. Zrobimy to wewnątrz funkcji, w której podpinaliśmy i inne elementy:


function lightbox() {
    ...

    function bindEvents() {
        ...

        DOM.img.addEventListener("load", () => {
            hideLoading();
        });
    }

    ...
}

Obsługa błędów

Nie zawsze zdjęcie musi się wczytać. Tak samo jak pokazywaliśmy ikonkę wczytywania, w razie konieczności pokażmy element z komunikatem błędu:


function lightbox() {
    ...

    function showError() {
        const div = document.createElement("div");
        div.classList.add("lightbox-img-error");
        div.innerHTML = "Błąd wczytywania grafiki";
        DOM.imgCnt.append(div);
    }

    function hideError() {
        if (DOM.imgCnt.querySelector(".lightbox-img-error")) {
            DOM.imgCnt.querySelector(".lightbox-img-error").remove();
        }
    }

    function bindEvents() {
        ...

        DOM.img.addEventListener("load", () => {
            hideLoading();
            hideError();
        });

        DOM.img.addEventListener("error", () => {
            hideLoading();
            showError();
        });
    }

    ...
}

Ładniejsze pokazywanie lightboxa

W tej chwili nasz lightbox pojawia się i znika "na chama", ponieważ jest wstawiany i usuwany z dokumentu. Ale my chcemy mieć doskonały lightbox, który będzie pojawiał się płynnie. Poprawmy to lekko przerabiając funkcje showLightbox() i hideLightbox() dodając do nich animację korzystając z api animate:


function lightbox() {
    ...

    function showLightbox() {
        DOM.lightbox.style.opacity = 0;
        document.body.append(DOM.lightbox);
        const anim = DOM.lightbox.animate([
            {opacity: 0},
            {opacity: 1}
        ], {
            duration: 200
        })
        anim.onfinish = () => DOM.lightbox.style.opacity = 1;
    }

    function hideLightbox() {
        const anim = DOM.lightbox.animate([
            {opacity: 1},
            {opacity: 0}
        ], {
            duration: 200
        })
        anim.onfinish = () => DOM.lightbox.remove();
    }

    ...
}

Opcje

Dodajmy też dodatkowe opcje, które pozwolą nam w przyszłości sterować działaniem naszego lightboxa.


function Lightbox(opts) {
    const defaultOptions = {
        showCount : true, //czy pokazywać licznik
        showText : true, //czy pokazywać opis zdjęcia
        closeOnBg : false, //czy zamykać poprzez kliknięcie na bg
        prevText : "Poprzednie zdjęcie",
        nextText : "Następne zdjęcie",
        closeText : "Zamknij",
        errorText : "Błąd wczytywania grafiki"
    }
    const options = { ...defaultOptions, ...opts, }

    ...

    function showError() {
        const div = document.createElement("div");
        div.classList.add("lightbox-img-error");
        div.innerHTML = options.errorText;
        DOM.imgCnt.append(div);
    }

    ...

    function showImage(src, text = "") {
        ...
        if (options.showCount) showCount();
    }

    ...

    function bindEvents() {
        ...

        if (options.closeOnBg) {
            //po kliknięciu na tło lightboxa zamykam go
            DOM.lightbox.addEventListener("click", () => {
                hideLightbox();
            })
            //równocześnie nie chcę by kliknięcie na zdjęcie zamykało więc przerywać wędrówkę zdarzenia
            DOM.imgCnt.addEventListener("click", e => e.stopPropagation());
        }

        ...
    }

    ...

    function addLinks(selector) {
        ...

        if (links.length > 1) {
            if (options.showCount) showCount();
            showPrevNext();
        } else {
            hideCount();
            hidePrevNext();
        }
    }

    function removeLinks(selector) {
        ...

        if (links.length > 1) {
            if (options.showCount) showCount();
            showPrevNext();
        } else {
            hideCount();
            hidePrevNext();
        }
    }

    ...
}

Wywołanie lightboxa

I na tym zakończymy. Aby teraz użyć naszego lightboxa, skorzystamy z kodu:


const lightbox = lightbox({
    showCount : false,
    showText : false,
    closeOnBg : false,
});

//możemy dodawać zdjęcia porcjami
lightbox.addLinks(".gallery-el");
lightbox.addLinks(".other-gallery-el");

//ale możemy też usuwać już dodane
lightbox.removeLinks(".no-this-link");

//mamy też metody które w razie czego posłużą nam do pokazywania i ukrywania odpowiednich elementów
lightbox.showText();
lightbox.showCount();

//plus takich lightboxów możemy mieć kilka na stronie
//dla każdej galerii mogą zachowywać się inaczej
const lightbox2 = lightbox({
    showCount : true,
    showText : true,
    closeOnBg : true,
});
lightbox.addLinks(".gallery2-el");

Miniaturki

Skoro nam tak dobrze idzie, pokuśmy się o dodanie miniaturek pod lightboxem. Jak to zrobić?

Po pierwsze podczas generowania naszego lightboxa musimy dodać odpowiednie elementy do niego:


function lightbox() {
    ...

    function createLightbox() {
        const lightbox = document.createElement("div");
        lightbox.classList.add("lightbox");
        lightbox.innerHTML = `
            <div class="lightbox-box">
                ...
            </div>
            <div class="lightbox-thumbnails">
                <ul class="lightbox-thumbnails-list"></ul>
            </div>
        `;
        return lightbox;
    }

    function getElementsReference() {
        const ob = {};
        ...
        ob.thumbnails = ob.lightbox.querySelector(".lightbox-thumbnails");
        ob.thumbnailsList = ob.lightbox.querySelector(".lightbox-thumbnails-list");
        return ob;
    }
    ...
}

Dodaliśmy dwa elementy .lightbox-thumbnails i .lightbox-thumbnails-list, ponieważ potrzebujemy ich do stylowania by wycentrować miniaturki w poziomie.

Element .lightbox-thumbnails będziemy pokazywać lub ukrywać, natomiast do .lightbox-thumbnails-list wstawimy miniaturki.

Dodajmy więc funkcję generującą odpowiedni kod i użyjmy jej przy dodawaniu lub odejmowaniu linków:


function lightbox() {
    ...
    const options = {
        ...
        showThumbnails : true,
        ...
    }

    function makeThumbnails() {
        DOM.thumbnailsList.innerHTML = "";
        links.forEach(link => {
            const thumb = document.createElement("a");
            thumb.href = link.href;
            thumb.classList.add("lightbox-thumbnails-el");
            thumb.dataset.text = link.dataset.text;
            const img = document.createElement("img");
            img.src = link.href;
            thumb.append(img);
            DOM.thumbnailsList.append(thumb);
        })
    }

    ...

    function addLinks(selector) {
        ...

        makeThumbnails();

        if (links.length > 1) {
            if (options.showCount) showCount();
            showPrevNext();
            if (options.showThumbnails) showThumbnails();
        } else {
            hideCount();
            hidePrevNext();
            if (options.showThumbnails) hideThumbnails();
        }
    }

    function removeLinks(selector) {
        ...

        makeThumbnails();

        if (links.length > 1) {
            if (options.showCount) showCount();
            showPrevNext();
            if (options.showThumbnails) showThumbnails();
        } else {
            hideCount();
            hidePrevNext();
            if (options.showThumbnails) hideThumbnails();
        }
    }
}

Dodajmy też funkcje do pokazywania i krywania miniaturek - tak samo jak to zrobiliśmy dla licznika i przycisków poprzedni/następny:


function lightbox() {
    ...

    function hideThumbnails() {
        DOM.thumbnails.style.display = "none";
        DOM.lightbox.classList.remove("lightbox--gallery");
    }

    function showThumbnails() {
        DOM.thumbnails.style.display = "block";
        DOM.lightbox.classList.add("lightbox--gallery");
    }

    ...

    return {
        addLinks,
        removeLinks,
        hidePrevNext,
        showPrevNext,
        hideText,
        showText,
        hideCount,
        showCount,
        showThumbnails,
        hideThumbnails,
    }
}

Zauważyłeś, że poza pokazywaniem/ukrywaniem miniaturek dodajemy do lightboxa dodatkową klasę? Wynika ona z jednego z dziwnych zachowań CSS. Jeżeli wstawisz img (nasz duży obrazek) do jakiegoś elementu, to ciężko jest wymusić na tym obrazku by miał max-height: 100%. Żadne flexy, gridy tutaj nie działają (przynajmniej ja nie znalazłem na to sposobu). Pozostaje więc dać mu maksymalną wysokość korzystając z innej jednostki.

Dodajmy do stylowania dodatkowe style:


...

.lightbox {
    ...
}

.lightbox--gallery .lightbox-img {
    max-height: calc(100vh - 220px);
}

...

.lightbox-thumbnails {
    text-align: center;
    overflow: hidden;
}

.lightbox-thumbnails-list {
    overflow-x: auto;
    list-style: none;
    padding: 0;
    margin: 0;
    display: inline-flex;
    max-width: 100%;
}

.lightbox-thumbnails-list::-webkit-scrollbar {
    width: 2px;
    background: #000;
    height: 2px;
    border-radius: 2px;
}

.lightbox-thumbnails-list::-webkit-scrollbar {
    width: auto;
}

.lightbox-thumbnails-list::-webkit-scrollbar-thumb {
    background-color: #FFF;
    border-radius: 2px;
}

.lightbox-thumbnails-el {
    position: relative;
    margin: 5px;
    overflow: hidden;
    border-radius: 3px;
    flex-shrink: 0;
}

.lightbox-thumbnails-el img {
    width: 90px;
    height: 90px;
    object-fit: cover;
    display: block;
}

...

Aktywna miniaturka

Z pewnością będziesz chciał wskazać użytkownikowi, które zdjęcie aktualnie pokazujesz.

Zmodyfikujmy nieco nasze stylowanie dodając klasę .is-active dla aktywnej miniaturki:


...

.lightbox-thumbnails-el.is-active::before {
    content: "";
    pointer-events: none;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    box-shadow: 0 0 0 3px #FFF inset;
    z-index: 1;
}

.lightbox-thumbnails-el.is-active img {
    filter: contrast(1.3);
}

Napiszmy też funkcję, która doda tą klasę do odpowiedniego elementu:


function lightbox() {
    ...

    function setActiveThumbnail() {
        const thumbnails = [...DOM.thumbnailsList.children];
        thumbnails.forEach(link => {
            link.classList.remove("is-active");
        });
        thumbnails[currentIndex].classList.add("is-active");
    }

    ...
}

A następnie podepnijmy klikanie na miniaturki:


function lightbox() {
    ...

    function bindEvents() {
        ...

        DOM.thumbnails.addEventListener("click", e => {
            const el = e.target.closest(".lightbox-thumbnails-el");
            if (el) {
                e.preventDefault();
                currentIndex = [...DOM.thumbnailsList.children].indexOf(el);
                showImage(el.href, el.getAttribute("data-text") ? el.getAttribute("data-text") : " ")
            }
        })
    }

    ...
}

Demo

demo

Bonus: fix dla mobilek

Przetestowałem nasze dzieło na różnych przeglądarkach na swoim leciwym telefonie. Nie zaskoczyło mnie to, że klasycznie pojawił się problem z "pełną wysokością ekranu", która na urządzeniach mobilnych bardzo często sprawia problemy (nie bierze pod uwagę pasków np. z narzędziami). I tak na mobilnym Chromie czy Firefoxie nasze dzieło działało całkiem dobrze, natomiast mobilne Vivaldi (super przeglądarka!) pozwala dodać na dole ekranu dodatkowy pasek narzędzi, który przykrywa nasze miniaturki. Jest to spowodowane tym, że 100vh na urządzeniach mobilnych nie uwzględnia pasków, więc obszar jest nieco większy. Teoretycznie jest na to wartość -webkit-fill-available, ale nie znalazłem przykładu jak jej użyć z calc(). Pozostaje więc wykorzystać Javascript, którym pobierzemy aktualną wysokość ekranu a następnie dokonamy odpowiednich wyliczeń.

Stwórzmy do tego odpowiednią funkcję i jej użyjmy:


function lightbox() {
    function getElementsReference() {
        const ob = {};
        ob.lightbox = createLightbox();
        ob.count = ob.lightbox.querySelector(".lightbox-count");
        ob.box = ob.lightbox.querySelector(".lightbox-box"); //ten element musimy ustawiać
        ...
        return ob;
    }

    function correctBoxHeight() {
        const boxMaxHeight = window.innerHeight - DOM.thumbnails.offsetHeight - DOM.text.offsetHeight - DOM.count.offsetHeight;
        DOM.box.style.maxHeight = `${boxMaxHeight}px`;
    }

    function showLightbox() {
        DOM.lightbox.style.height = `${window.innerHeight}px`;
        DOM.lightbox.style.pointerEvents = 'none';
        DOM.lightbox.style.opacity = 0;
        document.body.append(DOM.lightbox);
        correctBoxHeight();
        ...
    }

    function hideText() {
        ...
        correctBoxHeight();
    }

    function showText() {
        ...
        correctBoxHeight();
    }

    function hideCount() {
        ...
        correctBoxHeight();
    }

    function showCount() {
        ...
        correctBoxHeight();
    }

    function hideThumbnails() {
        ...
        correctBoxHeight();
    }

    function showThumbnails() {
        ...
        correctBoxHeight();
    }

    ...
}

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.