Slider

W poniższym tekście postaramy się zrobić dynamiczny slider.

Zobacz demo

Zaczynamy od planu. Nasz slider powinien mieć poprawną strukturę html. Powinien też mieć przyciski "poprzedni-następny" do zmieniania slajdów, oraz kropki na dole - także służące do zmiany slajdów. Dodatkowo powinien mieć możliwość ustawiania kilku opcji i użycia kilka razy na tej samej stronie.

Stworzenie prostego animowanego slidera nie jest ciężką rzeczą. Nie jest, jeżeli stworzysz odpowiednią strukturę w kodzie html. Jeżeli tutaj popełnisz błąd czeka cię potem ciężka mozolna praca.

Jak więc ugryźć taki slider? Jego działanie będzie się opierać na zasadzie, którą opisałem we wpisie http://domanart.pl/animacje-css/. Jeżeli jakiś element ma podstawowe stylowanie i właściwość transition, to poprze dodatkową klasę możemy to stylowanie płynnie zmieniać.
Przykładowe zastosowanie tej techniki pokazuje poniższy kod. Dodajemy mu poprzez JavaScript dodatkową klasę, która płynnie zmieni jego właściwości. W poniższym przykładzie element domyślnie jest niewidoczny. Jeżeli dostanie klase active, płynnie się pokaże, a jego tytuł wjedzie na planszę:


.slide {
    opacity: 0;
    z-index: 0;
    position: relative;
    transition:2s all;
}
.slide .title {
    left: -2rem;
    opacity: 0;
}

/* poprzez dodatkową klasę zmieniam właściwości elementu i jego dzieci */
.slide.active {
    opacity: 1;
    z-index: 1;
}
.slide.active .title {
    left: 0;
    opacity: 1;
}

Wracając do naszego slidera. Podobnie jak w powyższym kodzie wszystkim slajdom i elementom w nich damy podstawowe stylowanie. Aktualnie wyświetlanemu slajdowi dynamicznie dodamy dodatkową klasę, która to podstawowe stylowanie zmieni - np. przestawi opacity z 0 na 1.

Zanim przejdziemy do manipulacjami slajdami, stwórzmy podstawową strukturę html:


<div class="elements-list" id="slider1">
    <article class="element">
        <h2 class="element-title">
            1 Lorem ipsum dolor sit.
        </h2>
        <div class="element-text">
            Lorem ipsum dolor sit amet, consectetur adipisicing elit...
        </div>
    </article>
    <article class="element">
        <h2 class="element-title">
            2 Lorem ipsum dolor sit.
        </h2>
        <div class="element-text">
            Lorem ipsum dolor sit amet, consectetur adipisicing elit...
        </div>
    </article>
</div>

Jak widzisz jest to zwykła lista artykułów? Powiedzmy, że jakiś elementów. Co w takich elementach się znajdzie zależy od nas samych. W naszych przykładach będzie to tylko tytuł i tekst, ale nic nie stoi na przeszkodzie by dodać dodatkowe elementy.

Powyższą strukturę musimy zamienić na slider. Będzie on miał strukturę taką jak na poniższej grafice:

Struktura html slidera

Całość musimy zrobić za pomocą JavaScript, czyli musimy dynamicznie wygenerować:

  • przyciski .slider-button-prev, .slider-button-next
  • kropki .slider-dots
  • powyższe artykuły zamieninić na slajdy i wrzucić je do wspólnego elementu .slider-slider-cnt

A czemu właściwie tak, a nie od razu zrobić strukturę slidera w html? Z 2 powodów. Nasz slider będzie pluginem zmieniającym dany html na slider. Ale przecież ktoś mógł by użyć innego pluginu, który np. zmienił by powyższy kod na acordiona albo taby?

Wszystkie slajdy będą się znajdować w tym samym miejscu, musimy je więc pozycjonować absolutnie względem rodzica. Tym rodzicem jest element .slider-slide-cnt, który musi mieć pozycjonowanie relatywne.

Po zamianie za pomocą JavaScript powyższy kod html zamieni się na poniższy kod slidera:


<div class="slider elements-list" id="slider1">
    <div class="slider-slides-cnt">
        <article class="element slider-slide">
            <h2 class="element-title">
                1 Lorem ipsum dolor sit.
            </h2>
            <div class="element-text">
                Lorem ipsum dolor sit amet, consectetur adipisicing elit...
            </div>
        </article>
        <article class="element slider-slide">
            <h2 class="element-title">
                2 Lorem ipsum dolor sit.
            </h2>
            <div class="element-text">
                Lorem ipsum dolor sit amet, consectetur adipisicing elit...
            </div>
        </article>
    </div>
    <div class="slider-nav">
        <button type="button" class="slider-button slider-button-prev">Poprzedni</button>
        <button type="button" class="slider-button slider-button-next">Następny</button>
    </div>
    <ul class="slider-dots">
        <li class="slider-dots-element slider-dots-element-active"><button class="slider-dots-button" type="button">1</button></li>
        <li class="slider-dots-element"><button class="slider-dots-button" type="button">2</button></li>
        <li class="slider-dots-element"><button class="slider-dots-button" type="button">3</button></li>
    </ul>
</div>

Od razu napiszmy stylowanie dla naszego slidera. Grafikę tła pobrałem ze strony https://www.pexels.com/:


.slider {
    position: relative;
    background: url(slide-bg1.jpeg);
    background-size: cover;
    background-position: center center;
}

/* kontener dla wszystkich slajdów */
.slider-slides-cnt {
    height:700px;
    position: relative;
}

/* pojedynczy slide */
.slider-slide {
    position: absolute;
    left:0;
    top:0;
    width:100%;
    height:100%;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    opacity:0;
    z-index: 0;
}
/* slide aktywny */
.slider-slide-active {
    z-index: 1;
}

/* elementy pojedynczego slajdu */
.slider .element-title {
    color:#fff;
    font-family: sans-serif;
    position: relative;
    left:-3rem;
    opacity:0;
    font-size:6rem;
    text-transform: uppercase;
    font-weight: normal;
    margin-top:0;
    text-align: center;
    font-family: 'Roboto Condensed', sans-serif;
}
.slider .element-text {
    color:#fff;
    position: relative;
    left:-3rem;
    opacity:0;
    font-size:1.5rem;
    color:rgba(255,255,255,.6);
    padding-left:4rem;
    padding-right:4rem;
    text-align: center;
    font-family: serif;
    font-style: italic;
    max-width:60%;
}

/* animacja slajdów */
.slider-slide {
    opacity: 0;
    transition:0.4s all;
}
.slider .element-title {
    left:-3rem;
    opacity:0;
    transition: 2s left, 2s opacity;
}
.slider .element-text {
    left:-3rem;
    opacity:0;
    transition: 2s 1s left, 2s 1s opacity;
}

/* aktywny slide zmienia właściwości elementów */
.slider-slide-active {
    opacity: 1;
}
.slider-slide-active .element-title {
    left:0;
    opacity:1;
}
.slider-slide-active .element-text {
    left:0;
    opacity:1;
}

/* przyciski strzałek slajdu */
.slider-button-prev,
.slider-button-next {
    cursor: pointer;
    z-index: 2;
    position: absolute;
    left:0;
    top:0;
    width:4rem;
    height:100%;
    background: rgba(0,0,0,0.1);
    border:0;
    text-indent:-999px;
    overflow:hidden;
}
.slider-button-next {
    left:auto;
    right:0;
}
.slider-button-prev:focus,
.slider-button-next:focus {
    outline: none;
}
.slider-button-prev:before,
.slider-button-next:before {
    content:'<';
    display: flex;
    justify-content: center;
    align-items: center;
    position: absolute;
    left:0;
    top:0;
    width:100%;
    height:100%;
    font-size:2rem;
    color:#fff;
    text-indent: 0;
}
.slider-button-next:before {
    content: '>';
}

/* slider dots */
.slider-dots {
    z-index: 2;
    position: absolute;
    left:0;
    bottom:1rem;
    margin:0;
    padding:0;
    width:100%;
    display: flex;
    justify-content: center;
    list-style:none;
}
.slider-dots-element {
}
.slider-dots-button {
    background: transparent;
    width:2rem;
    height:2rem;
    border:0;
    text-indent:-999px;
    overflow: hidden;
    cursor: pointer;
    position: relative;
}
.slider-dots-button:before {
    content:'';
    position: absolute;
    left:50%;
    top:50%;
    transform:translate(-50%, -50%);
    background: transparent;
    width:0.9rem;
    height:0.9rem;
    border-radius: 50%;
    transition:0.5s all;
    box-shadow:inset 0 0 0 2px rgba(255,255,255,0.5);
}
.slider-dots-element-active .slider-dots-button:before {
    background: #fff;
    transform:translate(-50%, -50%) scale(1.2);
}
.slider-dots-button:focus {
    outline: none;
}

Konstruktor Slide

Założeniem jest, że slajdów możemy mieć kilka na stronie, więc powinniśmy zrobić je pod wspólny wzór - czyli skorzystamy z konstruktora lub klas. Ja w poniższym kodzie użyłem klasycznego podejścia, ale nic nie stoi na przeszkodzie by poniższy kod przerobić na klasy.

Pierwszą czynnością jest stworzenie odpowiednich zmiennych:


const Slider = function(elemSelector) {
    this.currentSlide = 0; //aktualny slide
    this.sliderSelector = elemSelector; //selektor elementu który zamienimy na slider
    this.elem = null; //tutaj pobierzemy element ktory zamienimy na slider
    this.slider = null; //tutaj wygenerujemy slider
    this.slides = null; //tutaj pobierzemy slajdy
    this.prev = null; //przycisk prev
    this.next = null; //przucisl next
    this.dots = []; //przyciski kropek
}

Metoda generateSlider()

Pierwszą metodą jaką napiszemy to metoda zamieniająca powyższą strukturę html na odpowiednią.


Slider.prorotype.generateSlider = function() {
    //pobieramy element który zamienimy na slider
    this.slider = document.querySelector(this.sliderSelector);
    this.slider.classList.add('slider');

    //tworzymy kontener dla slajdow
    const slidesCnt = document.createElement('div');
    slidesCnt.classList.add('slider-slides-cnt');

    //pobieramy element slajdów
    this.slides = this.slider.children;

    //to jest żywa kolekcja, więc przy przeniesieniu kazdego slajda
    //jej dlugosc maleje
    while (this.slides.length) {
        this.slides[0].classList.add('slider-slide');
        //jeżeli element appendujemy do innego elementu
        //to tak jakbyśmy go usuwali z tej kolekcji
        //bo jeden element nie może być równocześnie w 2 miejscach
        slidesCnt.appendChild(this.slides[0]);
    }

    //musimy na nowo pobrać slajdy, bo powyższa kolekcja jest już pusta
    this.slides = slidesCnt.querySelectorAll('.slider-slide');

    //wygenerowaliśmy kontener ze slajdami, wstawiamy go więc do slidera
    this.slider.appendChild(slidesCnt);
}

Generujemy element .slider-slides-cnt, do którego wstawiamy slajdy czyli bezpośrednie dzieci elementu który wskażemy w zmiennej elemSelector w konstruktorze. W naszym przykładzie będą to elementy article.

Żeby takie slajdy wstawić robimy po nich pętlę i każdorazowo appendujemy każdy z nich do elementu .slider-slides-cnt. Elementy pobrane za pomocą właściwości children stanowią żywą kolekcję. Oznacza to, że jeżeli z takiej kolekcji usuniemy element czy przeniesiemy go w inne miejsce, kolekcja automatycznie się zaktualizuje, a jej długość się zmieni. My pobieramy dzieci wskazanego elementu (.elements-list). Jeżeli teraz w pętli każdorazowo każde takie dziecko przeniesiemy do innego elementu (u nas do .slider-slides-cnt) to kolekcja children już nie będzie zawierać takiego dziecka (bo pojedynczy element nie może być równocześnie w 2 miejscach).

Poniższy kod pokazuje zasadę działania:


const div1 = document.querySelector('#div1');
const div2 = document.querySelector('#div2');

const divs = div1.children; //pobraliśmy 3 dzieci
console.log(divs.length); //3

div2.appendChild(children[0]); //jedno z dzieci dołączamy do 2 diva

console.log(divs.length); //2 //kolekcja się zmniejszyła bo wskazywała na dzieci 1 diva

Powyższą metodę generateSlider() od razu odpalamy w konstruktorze:


const Slider = function(elemSelector) {
    this.currentSlide = 0; //aktualny slide
    this.sliderSelector = elemSelector; //selektor elementu który zamienimy na slider
    this.elem = null; //tutaj pobierzemy element ktory zamienimy na slider
    this.slider = null; //tutaj wygenerujemy slider
    this.slides = null; //tutaj pobierzemy slajdy
    this.prev = null; //przycisk prev
    this.next = null; //przucisl next
    this.dots = []; //przyciski kropek

    this.generateSlider();
}

Metoda createPrevNext()

Kontynuujemy generowanie struktury html. Kolejną metodą będzie createPrevNext(), która stworzy dla naszego slidera przyciski "Poprzedni i następny slide":


Slider.prototype.createPrevNext = function() {
    this.prev = document.createElement('button');
    this.prev.type = "button";
    this.prev.innerText = "Poprzedni slide";
    this.prev.classList.add('slider-button');
    this.prev.classList.add('slider-button-prev');
    this.prev.addEventListener('click', this.slidePrev.bind(this));

    this.next = document.createElement('button');
    this.next.type = "button";
    this.next.innerText = "Następny slide";
    this.next.classList.add('slider-button');
    this.next.classList.add('slider-button-next');
    this.next.addEventListener('click', this.slideNext.bind(this));

    const nav = document.createElement('div');
    nav.classList.add('slider-nav');
    nav.setAttribute('aria-label', 'Slider prev next');
    nav.appendChild(this.prev);
    nav.appendChild(this.next);
    this.slider.appendChild(nav);
}

Żeby mieć w klikniętym przycisku dostęp do właściwości metod naszego slidera, musimy je przypiąć za pomocą metod bind(). Cała reszta kodu to stworzenie 2 przycisków i ustawienie im podstawowych właściwości.

Po kliknięciu na te przyciski odpalimy właściwości slidePrev() i slideNext(). Napiszemy je za chwilę.

Powyższą metodę odpalmy na końcu metody generateSlider:


Slider.prorotype.generateSlider = function() {
//pobieramy element który zamienimy na slider
    this.slider = document.querySelector(this.sliderSelector);
    this.slider.classList.add('slider');

    //tworzymy kontener dla slajdow
    const slidesCnt = document.createElement('div');
    slidesCnt.classList.add('slider-slides-cnt');

    //pobieramy element slajdów
    this.slides = this.slider.children;

    //to jest żywa kolekcja, więc przy przeniesieniu każdego slajda
    //jej dlugosc maleje
    while (this.slides.length) {
        this.slides[0].classList.add('slider-slide');
        slidesCnt.appendChild(this.slides[0]);
    }

    //musimy na nowo pobrać slajdy, bo powyższa kolekcja jest już pusta
    this.slides = slidesCnt.querySelectorAll('.slider-slide');

    //wygenerowaliśmy kontener ze slajdami, wstawiamy go więc do slidera
    this.slider.appendChild(slidesCnt);

    this.createPrevNext();
}

Metoda createDots()

Kolejną czynnością jest stworzenie metody generującej kropki zmieniające slajdy:


Slider.prototype.createDots = function() {
    const ulDots = document.createElement('ul');
    ulDots.classList.add('slider-dots');
    ulDots.setAttribute('aria-label', 'Slider pagination');

    //tworzymy pętlę w ilości liczby slajów
    for (let i=0; i<this.slides.length; i++) {
        //każdorazowo tworzymy LI wraz z buttonem
        //każdy button po kliknięciu zmieni slajd
        //za pomocą metody changeSlide()

        const li = document.createElement('li');
        li.classList.add('slider-dots-element');

        const btn = document.createElement('button');
        btn.classList.add('slider-dots-button');
        btn.type = "button";
        btn.innerText = i+1;
        btn.setAttribute('aria-label', 'Set slide '+(i+1));

        btn.addEventListener('click', function() {
            this.changeSlide(i);
        }.bind(this));

        li.appendChild(btn);

        ulDots.appendChild(li);

        //wygenerowany przycisk wrzucamy do wspólnej tablicy
        //dzięki temu potem łatwiej będzie nam się do tych kropek odwoływać
        this.dots.push(li);
    }

    this.slider.appendChild(ulDots);
}

Tak samo jak przy metodzie createPrevNext() powyższą metodę też odpalamy w metodzie generateSlider:


Slider.prorotype.generateSlider = function() {
    ...

    this.createPrevNext();
    this.createDots();
}

Odpalmy nasz slider i zobaczmy co w tej chwili mamy:


const slide = new Slider('#slider1');
Demo 1 - wygenerowana struktura html

Metoda changeSlide()

Po kliknięciu na kropkę odpalamy metodę changeSlide(), która ma przełączyć slajd. Metoda usuwa więc wszystkim slajdom klasę .slider-slide-active i dodaje ją tylko aktualnemu. Żeby zrobić pętlę forEach po kolekcji divów, skorzystamy ze sztuczki, którą poznaliśmy tutaj:


Slider.prototype.changeSlide = function(index) {
    //robimy pętlę po slajdach usuwając klasę active
    [].forEach.call(this.slides, function(slide) {
        slide.classList.remove('slider-slide-active');
        slide.setAttribute('aria-hidden', true);
    });

    //dodajemy ją tylko wybranemu
    this.slides[index].classList.add('slider-slide-active');
    this.slides[index].setAttribute('aria-hidden', false);

    //podobny manewr robimy dla kropek
    this.dots.forEach(function(dot) {
        dot.classList.remove('slider-dots-element-active');
    });
    this.dots[index].classList.add('slider-dots-element-active');

    //aktualny slide przestawiamy na wybrany
    this.currentSlide = index;
}

Dodatkowo wykorzystajmy tą metodę do ustawienia początkowego slajdu odpalając ją na końcu konstruktora:


const Slider = function(elemSelector) {
    this.sliderSelector = elemSelector;
    this.currentSlide = 0; //aktualny slide
    this.time = null; //tutaj będziemy podczepiać setTimeout
    this.slider = null;
    this.elem = null;
    this.slides = null;

    this.prev = null; //przycisk prev
    this.next = null; //przucisl next
    this.dots = [];

    this.generateSlider();
    this.changeSlide(this.currentSlide);
}

Metody slidePrev() i slideNext()

Powyższa metoda zmienia slajd. Klikając na przyciski "poprzedni-następny" też będziemy zmieniać slajd, tylko nie na konkretny numer, a o jeden więcej lub mniej.

Jak spojrzysz na początkowe zmienne, mamy tam zmienną currentSlide. Określa ona aktualny slajd.

Chcemy by przyciski "poprzedni-następny" działały w pętli. Jeżeli dojdziemy do ostatniego slajdu, to kolejne przełączenie powinno przestawić slider na pierwszy slajd. I w drugą stronę - jeżeli klikniemy poprzedni będąc na pierwszym slajdzie, powinien się wyświetlić ostatni. Wykorzystajmy do tego wspomnianą zmienną:


Slider.prototype.slidePrev = function() {
    this.currentSlide--;
    if (this.currentSlide < 0) {
        this.currentSlide = this.slides.length - 1;
    }
    this.changeSlide(this.currentSlide);
}

Slider.prototype.slideNext = function() {
    this.currentSlide++;
    if (this.currentSlide > this.slides.length - 1) {
        this.currentSlide = 0;
    }
    this.changeSlide(this.currentSlide);
}

W poniższym przykładzie lekko zmieniłem za pomocą styli czcionkę i tło:

Demo 2 - działające przyciski

Automatyzacja

Do automatycznego przełączania slajdów możemy wykorzystać setTimeout(). Wystarczy za pomocą tej funkcji wykonać powyższą metodę slideNext().

Pytanie które się pojawia to gdzie takie wywołanie zastosować?

W metodzie init odpalamy metodę changeSlide - tak by po wejściu na stronę był widoczny pierwszy slajd. Po kliknięciu na przyciski poprzedni-następny też wywołujemy metodę changeSlide. Jeżeli chcemy by po zmianie slajdu kolejna zmiana nastąpiła automatycznie, wystarczy na końcu tej metody dodać nasz setTimeout, który wywoła nam jeszcze raz metodę changeSlide():


Slider.prototype.changeSlide = function(index) {
    [].forEach.call(this.slides, function(slide) {
        slide.classList.remove('slider-slide-active');
        slide.setAttribute('aria-hidden', true);
    });

    this.slides[index].classList.add('slider-slide-active');
    this.slides[index].setAttribute('aria-hidden', false);

    if (this.options.generateDots) {
        this.dots.forEach(function(dot) {
            dot.classList.remove('slider-dots-element-active');
        });
        this.dots[index].classList.add('slider-dots-element-active');
    }

    this.currentSlide = index;

    clearInterval(this.time);
    this.time = setTimeout(function() {
        this.slideNext();
    }.bind(this), 6000); //co 6 sekund automatycznie się przełączy
}

Jeżeli ktoś będzie szybko zmieniał slajdy (klikał po przyciskach lub kropkach), każdorazowo zostanie odpalone czyszczenie aktualnie odpalonego timeOuta i każdorazowo będzie on ustawiany na nowo. Dlatego nasze automatyczne przełączenie slajdu zadziała dopiero gdy użytkownik się uspokoi.

Opcje, więcej opcji

W powyższej metodzie ustawiliśmy przełączanie slajdów co 6 sekund. Aż prosi się, by przy wywołaniu naszego slidera programista mógł ustawić takie wartości.

Zastosujemy tutaj podejście, które dokładniej opisałem tutaj. Do naszego konstruktora trafi obiekt z opcjami, które ustawi programista. Opcje te nadpiszą domyślne ustawienia, w wyniku czego dostaniemy nowy scalony obiekt z ustawieniami slidera.


const opt = {
    width : 100
}

const default = {
    width: 10,
    height: 200
}

const options = Object.assign({}, default, opt);

console.log(options); //{width: 100, height:200}

Dodajmy domyślne ustawienia w konstruktorze:


const Slider = function(elemSelector, opts) {
    const defaultOpts = {
        pauseTime : 0
    };
    this.options = Object.assign({}, defaultOpts, opts);
    this.currentSlide = 0; //aktualny slide
    this.time = null; //tutaj będziemy podczepiać setTimeout
    this.slider = document.querySelector(elemSelector);
    this.slides = this.slider.querySelectorAll('.slider-slide');
    this.prev = null; //przycisk prev
    this.next = null; //przucisl next
    this.dots = [];

    this.generateSlider();
    this.changeSlide(this.currentSlide);
}

Domyślnie czas przerwy między kolejnym przełączaniem będzie wynosił 0. Jeżeli programista tego nie zmieni, nie będziemy automatycznie przełączać slajdów:


Slider.prototype.changeSlide = function(index) {
    [].forEach.call(this.slides, function(slide) {
        slide.classList.remove('slider-slide-active');
        slide.setAttribute('aria-hidden', true);
    });

    this.slides[index].classList.add('slider-slide-active');
    this.slides[index].setAttribute('aria-hidden', false);

    if (this.options.generateDots) {
        this.dots.forEach(function(dot) {
            dot.classList.remove('slider-dots-element-active');
        });
        this.dots[index].classList.add('slider-dots-element-active');
    }

    this.currentSlide = index;

    if (typeof this.options.pauseTime === "number" && this.options.pauseTime !== 0) {
        clearInterval(this.time);
        this.time = setTimeout(function() {
            this.slideNext();
        }.bind(this), this.options.pauseTime);
    }
}

Podobne opcje możemy ustawić dla innych elementów naszego slidera. Dla przykładu możemy dodać opcję do zmiany tekstów na przyciskach:


const Slider = function(elemSelector, opts) {
    const defaultOpts = {
        pauseTime : 0,
        prevText : "Poprzedni slide",
        nextText : "Następny slide"
    };
    this.options = Object.assign({}, defaultOpts, opts);
    this.currentSlide = 0; //aktualny slide
    this.time = null; //tutaj będziemy podczepiać setTimeout
    this.slider = document.querySelector(elemSelector);
    this.slides = this.slider.querySelectorAll('.slider-slide');
    this.prev = null; //przycisk prev
    this.next = null; //przucisl next
    this.dots = [];

    this.generateSlider();
    this.changeSlide(this.currentSlide);
}

Slider.prototype.createPrevNext = function() {
    this.prev = document.createElement('button');
    this.prev.type = "button";
    this.prev.innerText = this.options.prevText;
    this.prev.classList.add('slider-button');
    this.prev.classList.add('slider-button-prev');
    this.prev.addEventListener('click', this.slidePrev.bind(this));

    this.next = document.createElement('button');
    this.next.type = "button";
    this.next.innerText = this.options.nextText;
    this.next.classList.add('slider-button');
    this.next.classList.add('slider-button-next');
    this.next.addEventListener('click', this.slideNext.bind(this));

    const nav = document.createElement('div');
    nav.classList.add('slider-nav');
    nav.setAttribute('aria-label', 'Slider prev next');
    nav.appendChild(this.prev);
    nav.appendChild(this.next);
    this.slider.appendChild(nav);
}

Ostatnie dwie opcje, które dodamy to możliwość wyłączenia generowania przycisków "poprzeni-następny" jak i generowania kropek:


Slider.prototype.generateSlider = function() {
    //pobieramy element który zamienimy na slider
    this.slider = document.querySelector(this.sliderSelector);
    this.slider.classList.add('slider');

    //tworzymy kontener dla slajdow
    const slidesCnt = document.createElement('div');
    slidesCnt.classList.add('slider-slides-cnt');

    //pobieramy element slajdów
    this.slides = this.slider.children;

    //to jest żywa kolekcja, więc przy przeniesieniu kazdego slajda
    //jej dlugosc maleje
    while (this.slides.length) {
        this.slides[0].classList.add('slider-slide');
        slidesCnt.appendChild(this.slides[0]);
    }

    //musimy na nowo pobrać slajdy, bo powyższa kolekcja jest już pusta
    this.slides = slidesCnt.querySelectorAll('.slider-slide');

    //wygenerowaliśmy kontener ze slajdami, wstawiamy go więc do slidera
    this.slider.appendChild(slidesCnt);

    if (this.options.generatePrevNext) this.createPrevNext();
    if (this.options.generateDots) this.createDots();
}

Oraz w metodzie changeSlide(), która działa na kropkach.


Slider.prototype.changeSlide = function(index) {
    [].forEach.call(this.slides, function(slide) {
        slide.classList.remove('slider-slide-active');
        slide.setAttribute('aria-hidden', true);
    });

    this.slides[index].classList.add('slider-slide-active');
    this.slides[index].setAttribute('aria-hidden', false);

    if (this.options.generateDots) {
        this.dots.forEach(function(dot) {
            dot.classList.remove('slider-dots-element-active');
        });
        this.dots[index].classList.add('slider-dots-element-active');
    }

    this.currentSlide = index;

    if (typeof this.options.pauseTime === "number" && this.options.pauseTime !== 0) {
        clearInterval(this.time);
        this.time = setTimeout(function() {
            this.slideNext();
        }.bind(this), this.options.pauseTime)
    }
}

Od tej chwili programista używając naszego slidera będzie mógł podawać także jego opcje:


//slider1
const opts1 = {
    pauseTime : 0,
    prevText : "Poprzedni",
    nextText : "Następny"
}
const slide1 = new Slider('#slider1', opts);


//slider2
const opts1 = {
    pauseTime : 5000,
    generatePrevNext : false
}
const slide2 = new Slider('#slider2', opts);

Cały kod

Dotarliśmy do końca. Poniżej cały kod JavaScript (css się nie zmienił) i link do dema:


const Slider = function(elemSelector, opts) {
    const defaultOpts = {
        pauseTime : 0,
        prevText : "Poprzedni slide",
        nextText : "Następny slide",
        generateDots : true,
        generatePrevNext : true
    };
    this.options = Object.assign({}, defaultOpts, opts);
    this.sliderSelector = elemSelector;
    this.currentSlide = 0; //aktualny slide
    this.time = null; //tutaj będziemy podczepiać setTimeout
    this.slider = null;
    this.elem = null;
    this.slides = null;

    this.prev = null; //przycisk prev
    this.next = null; //przucisl next
    this.dots = [];

    this.generateSlider();
    this.changeSlide(this.currentSlide);
}

Slider.prototype.generateSlider = function() {
    //pobieramy element który zamienimy na slider
    this.slider = document.querySelector(this.sliderSelector);
    this.slider.classList.add('slider');

    //tworzymy kontener dla slajdow
    const slidesCnt = document.createElement('div');
    slidesCnt.classList.add('slider-slides-cnt');

    //pobieramy element slajdów
    this.slides = this.slider.children;

    //to jest zywa kolekcja, więc przy przeniesieniu kazdego slajda
    //jej dlugosc maleje
    while (this.slides.length) {
        this.slides[0].classList.add('slider-slide');
        slidesCnt.appendChild(this.slides[0]);
    }
    this.slides = slidesCnt.querySelectorAll('.slider-slide');
    this.slider.appendChild(slidesCnt);

    if (this.options.generatePrevNext) this.createPrevNext();
    if (this.options.generateDots) this.createDots();
}

Slider.prototype.slidePrev = function() {
    this.currentSlide--;
    if (this.currentSlide < 0) {
        this.currentSlide = this.slides.length - 1;
    }
    this.changeSlide(this.currentSlide);
}

Slider.prototype.slideNext = function() {
    this.currentSlide++;
    if (this.currentSlide > this.slides.length - 1) {
        this.currentSlide = 0;
    }
    this.changeSlide(this.currentSlide);
}

Slider.prototype.changeSlide = function(index) {
    [].forEach.call(this.slides, function(slide) {
        slide.classList.remove('slider-slide-active');
        slide.setAttribute('aria-hidden', true);
    });

    //dodajemy ją tylko wybranemu
    this.slides[index].classList.add('slider-slide-active');
    this.slides[index].setAttribute('aria-hidden', false);

    //podobny manewr robimy dla kropek
    if (this.options.generateDots) {
        this.dots.forEach(function(dot) {
            dot.classList.remove('slider-dots-element-active');
        });
        this.dots[index].classList.add('slider-dots-element-active');
    }

    //aktualny slide przestawiamy na wybrany
    this.currentSlide = index;

    if (this.options.pauseTime !== 0) {
        clearInterval(this.time);
        this.time = setTimeout(function() {
            this.slideNext();
        }.bind(this), this.options.pauseTime)
    }
}

Slider.prototype.createPrevNext = function() {
    this.prev = document.createElement('button');
    this.prev.type = "button";
    this.prev.innerText = this.options.prevText;
    this.prev.classList.add('slider-button');
    this.prev.classList.add('slider-button-prev');
    this.prev.addEventListener('click', this.slidePrev.bind(this));

    this.next = document.createElement('button');
    this.next.type = "button";
    this.next.innerText = this.options.nextText;
    this.next.classList.add('slider-button');
    this.next.classList.add('slider-button-next');
    this.next.addEventListener('click', this.slideNext.bind(this));

    const nav = document.createElement('div');
    nav.classList.add('slider-nav');
    nav.setAttribute('aria-label', 'Slider prev next');
    nav.appendChild(this.prev);
    nav.appendChild(this.next);
    this.slider.appendChild(nav);
}

Slider.prototype.createDots = function() {
    const ulDots = document.createElement('ul');
    ulDots.classList.add('slider-dots');
    ulDots.setAttribute('aria-label', 'Slider pagination');

    //tworzymy pętlę w ilości liczby slajów
    for (let i=0; i<this.slides.length; i++) {
        //każdorazowo tworzymy LI wraz z buttonem
        //każdy button po kliknięciu zmieni slajd
        //za pomocą metody changeSlide()

        const li = document.createElement('li');
        li.classList.add('slider-dots-element');

        const btn = document.createElement('button');
        btn.classList.add('slider-dots-button');
        btn.type = "button";
        btn.innerText = i+1;
        btn.setAttribute('aria-label', 'Set slide '+(i+1));

        btn.addEventListener('click', function() {
            this.changeSlide(i);
        }.bind(this));

        li.appendChild(btn);

        ulDots.appendChild(li);
        this.dots.push(li);
    }

    this.slider.appendChild(ulDots);
}

//wywołanie bez opcji
const slide = new Slider('#slider1');

//wywołanie z opcjami
const slide = new Slider('#slider1', {
    pauseTime : 10000,
    generateDots : true,
    generatePrevNext : true,
    prevText : "Poprzedni",
    nextText : "Następny"
});

Poniższy przykład - już końcowy - zawiera trzy różne slidery wraz z dodatkowym stylowaniem, którym zmieniłem ich wygląd. Dla strzałek zastosowałem https://www.bootstrapcdn.com/fontawesome/ wraz ikonami (lewo, prawo) dla strzałek

, oraz naprawdę lekkim efektem przy najechaniu na nie kursorem. Demo 3 - trzy slidery

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Przerób powyższy kod na klasy w ES6.
    
                    class Slider {
                        constructor(elemSelector, opts) {
                            const defaultOpts = {
                                pauseTime : 0,
                                prevText : "Poprzedni slide",
                                nextText : "Następny slide",
                                generateDots : true,
                                generatePrevNext : true
                            };
                            this.options = Object.assign({}, defaultOpts, opts);
                            this.sliderSelector = elemSelector;
                            this.currentSlide = 0; //aktualny slide
                            this.time = null; //tutaj będziemy podczepiać setTimeout
                            this.slider = null;
                            this.elem = null;
                            this.slides = null;
    
                            this.prev = null; //przycisk prev
                            this.next = null; //przucisl next
                            this.dots = [];
    
                            this.generateSlider();
                            this.changeSlide(this.currentSlide);
                        }
                        generateSlider() {
                            //pobieramy element który zamienimy na slider
                            this.slider = document.querySelector(this.sliderSelector);
                            this.slider.classList.add('slider');
    
                            //tworzymy kontener dla slajdow
                            const slidesCnt = document.createElement('div');
                            slidesCnt.classList.add('slider-slides-cnt');
    
                            //pobieramy element slajdów
                            this.slides = this.slider.children;
    
                            //to jest żywa kolekcja, więc przy przeniesieniu kazdego slajda
                            //jej długość maleje
                            while (this.slides.length) {
                                this.slides[0].classList.add('slider-slide');
                                slidesCnt.appendChild(this.slides[0]);
                            }
                            this.slides = slidesCnt.querySelectorAll('.slider-slide');
                            this.slider.appendChild(slidesCnt);
    
                            if (this.options.generatePrevNext) this.createPrevNext();
                            if (this.options.generateDots) this.createDots();
                        }
                        slidePrev() {
                            this.currentSlide--;
                            if (this.currentSlide < 0) {
                                this.currentSlide = this.slides.length - 1;
                            }
                            this.changeSlide(this.currentSlide);
                        }
    
                        slideNext() {
                            this.currentSlide++;
                            if (this.currentSlide > this.slides.length - 1) {
                                this.currentSlide = 0;
                            }
                            this.changeSlide(this.currentSlide);
                        }
    
                        changeSlide(index) {
                            //robimy pętlę po slajdach usuwając klasę active
                            [...this.slides].forEach(function(slide) {
                                slide.classList.remove('slider-slide-active');
                                slide.setAttribute('aria-hidden', true);
                            });
    
                            //dodajemy ją tylko wybranemu
                            this.slides[index].classList.add('slider-slide-active');
                            this.slides[index].setAttribute('aria-hidden', false);
    
                            //podobny manewr robimy dla kropek
                            if (this.options.generateDots) {
                                this.dots.forEach(function(dot) {
                                    dot.classList.remove('slider-dots-element-active');
                                });
                                this.dots[index].classList.add('slider-dots-element-active');
                            }
    
                            //aktualny slide przestawiamy na wybrany
                            this.currentSlide = index;
    
                            if (this.options.pauseTime !== 0) {
                                clearInterval(this.time);
                                this.time = setTimeout(function() {
                                    this.slideNext();
                                }.bind(this), this.options.pauseTime)
                            }
                        }
    
                        createPrevNext() {
                            this.prev = document.createElement('button');
                            this.prev.type = "button";
                            this.prev.innerText = this.options.prevText;
                            this.prev.classList.add('slider-button');
                            this.prev.classList.add('slider-button-prev');
                            this.prev.addEventListener('click', this.slidePrev.bind(this));
    
                            this.next = document.createElement('button');
                            this.next.type = "button";
                            this.next.innerText = this.options.nextText;
                            this.next.classList.add('slider-button');
                            this.next.classList.add('slider-button-next');
                            this.next.addEventListener('click', this.slideNext.bind(this));
    
                            const nav = document.createElement('div');
                            nav.classList.add('slider-nav');
                            nav.setAttribute('aria-label', 'Slider prev next');
                            nav.appendChild(this.prev);
                            nav.appendChild(this.next);
                            this.slider.appendChild(nav);
                        }
    
                        createDots() {
                            const ulDots = document.createElement('ul');
                            ulDots.classList.add('slider-dots');
                            ulDots.setAttribute('aria-label', 'Slider pagination');
    
                            //tworzymy pętlę w ilości liczby slajów
                            for (let i=0; i<this.slides.length; i++) {
                                //każdorazowo tworzymy LI wraz z buttonem
                                //każdy button po kliknięciu zmieni slajd
                                //za pomocą metody changeSlide()
    
                                const li = document.createElement('li');
                                li.classList.add('slider-dots-element');
    
                                const btn = document.createElement('button');
                                btn.classList.add('slider-dots-button');
                                btn.type = "button";
                                btn.innerText = i+1;
                                btn.setAttribute('aria-label', 'Set slide '+(i+1));
    
                                btn.addEventListener('click', function() {
                                    this.changeSlide(i);
                                }.bind(this));
    
                                li.appendChild(btn);
    
                                ulDots.appendChild(li);
                                this.dots.push(li);
                            }
    
                            this.slider.appendChild(ulDots);
                        }
                    }