Tworzymy Plugin do jQuery

Korzystając z jQuery, nie raz używamy różnego rodzajów pluginów, czyli rozszerzeń do tej wspaniałej biblioteki. W tym artykule nauczymy się sami tworzyć takie rozszerzenie pisząc naszą osobistą "nieskończoną karuzelę", czyli slider, który nie ma końca.

Rozszerzenia do jQuery można podzielić na dwie części. Jedne z nich rozszerzają jQuery nie operując na obiektach dokumentu:


    $.fn.square = function(x) {
        return x * x;
    }

    console.log($.kwadrat(5));

Drugie - bardziej nas interesujące operują na zbiorach elementów, które są do nich przekazywane. Przykładem takiej funkcji może myć np fadeOut(), który powoduje zanikanie obiektów, które zostały do niego przekazane.


    $('.someElements').fadeOut('slow');

Jak widać powyżej, funkcja fadeOut akceptuje (opcjonalnie) atrybuty określające szybkość jej działania. Nasz plugin powinien działać w taki sam sposób.

Ogólny schemat pluginów

Ogólny schemat większości pluginów ma postać:


(function($){
    $.fn.functionName = function(options) {
        options = $.extend({
            //...parametry
        }, options);

        return this.each(function() {
            //...metody, właściwości itp
        });
    }
})(jQuery);

Konstrukcja otaczająca nasz plugin (1 i 11 linijka) powoduje rzutowanie naszego kodu na jQuery. Dzięki temu unikamy niekompatybilności naszego kodu z innymi bibliotekami wykorzystującymi znak dolara (np. biblioteka prototype). Takie zabezpieczenie.

Nasz plugin powinien mieć określone parametry swojego działania, które użytkownik powinien móc nadpisać własnymi wartościami. Wykonujemy to w prosty sposób poprzez wykorzystanie instrukcji .extend:


(function($){
    $.fn.carousel = function(options) {

        var options = $.extend({
            displayElements : 3, //ile elementów będziemy pokazywać
            animationTime   : 700, //czas animacji
            pauseTime       : 3000, //przerwa między automatycznym przewijaniem                
            onScroll        : function() {} //opcjonalna funkcja zwrotna po przewinięciu jednego slajda
        }, options);

    }
})(jQuery);

W powyższym przykładzie zdefiniowaliśmy domyślne parametry naszego pluginu. W naszym przypadku noszą one nazwę "options", chociaż nazwa ta może być dowolna. Użytkownik wywołując nasz plugin ma możliwość nadpisania tych wartości np:

$('....').carousel({ animateTime : 200 });

Kolejną częścią każdego pluginu jest jego centralna część, która ma postać:


return this.each(function() {
    //...metody, właściwości ect = serce pluginu ;)
    var $t = $(this);
});

Przechodzi ona po wszystkich elementach, które zostały przekazane do naszego pluginu i dla każdego z nich wywołuje nasz plugin. Za każdą iteracją pod zmienną $t podstawiamy dany element strony. Tak więc:


$(function() {
    $('.superDiv').carousel()
});

spowoduje, że nasz plugin zostanie odpalony kolejno dla wszystkich .superDiv, a pod zmienną $t każdorazowo podstawione będą kolejne super divy.

Ogólny szkielet slidera

Stwórzmy teraz szkielet html naszej karuzeli, który potem potraktujemy naszym pluginem:


<div class="my_name_is_boxik">
    <span class="prev">&lt;</span>
    <div class="wrapper">
        <ul>
            <li>..1..</li>
            <li>..2..</li>
            <li>..3..</li>
            <li>..4..</li>
        </ul>
    </div>
    <span class="next">&gt;</span>
</div>

/* infinite carousel */
.carousel {
    clear: both;
    background: #eee;
    position: relative;
    width: 690px;
    height: 230px;
    padding:0 20px;
}

.carousel .wrapper {
    overflow: hidden;
    position: relative;
    width: 100%;
    height: 100%;
}

.carousel .wrapper ul {
    list-style-type: none;
    height: 100%;
    margin: 0;
    padding: 0;
    width: 9999px;
}

.carousel .wrapper ul li {
    float: left;
    width: 220px;
    margin: 5px;
    text-align: center;
    line-height:220px;
    font-size:30px;
    background: #fff;
    height: 220px;
    overflow: hidden;
}

.carousel .prev,
.carousel .next {
    cursor: pointer;
    position: absolute;
    overflow: hidden;
    width: 50px;
    height:50px;
    border-radius:50%;
    z-index: 2;
    top:50%;
    transform:translate(0, -50%);
    -webkit-transform:translate(0, -50%);
    font: bold 13px Tahoma;
    text-align: center;
    line-height: 50px;
    color:#fff;
    background: #EC185D;
}

.carousel .prev {
    left: -15px;
}

.carousel .next {
    right: -15px;
}

Tworzymy kod pluginu

Podstawmy powyższe elementy pod zmienne:


return this.each(function() {
    var $t = $(this);

    var $ul = $t.find('ul');
    var $li = $ul.find('li');
    var $prev = $t.find('.prev');
    var $next = $t.find('.next');
    var itemWidth = $li.first().outerWidth(true); //szerokość elementu - o tyle będzie przesuwał się slider
    var interval = null; //posłuży do automatycznego przewijania slidera

});

Podepnijmy teraz odpowiednie zdarzenia pod przyciski przesuwające (prev i next):


    $prev.on('click', scrollPrev);
    $next.on('click', scrollNext);

Pozostało nam już tylko napisać te zdarzenia.
Pierwsze na ostrzał pójdzie zdarzenie scrollPrev, które jak sama nazwa mówi będzie pokazywało wcześniejsze pozycje (LI).

W tym momencie przyda się kawałek kartki. Rysujemy!
Nasza karuzela będzie miała bardzo prostą zasadę. Jeżeli przewijamy listę w lewo, wtedy na pierwszą pozycję wrzucamy element z jej końca. Jeżeli listę przesuwamy w prawo, wtedy wrzucimy na jej koniec element z jej początku.

Przy przesuwaniu listy w lewo wykonamy następujące czynności:

  • Przesuniemy listę o 1 pole w lewo (0-item_width)
  • Wstawimy ostatni element listy na jej początek
  • Zaanimujemy przesunięcie listy z pozycji 0-item_width do pozycji 0

Aby zrealizować powyższe kroki musimy utworzyć lokalną metodę, która będzie przesuwać nasz scroller.


var scrollPrev = function() {
    if (!$ul.is(':animated')) {
        var $li = $ul.find('li');
        $ul.css('margin-left',-item_width);
        $li.first().before($li.last());
        $ul.animate({'margin-left' : 0}, options.animationTime, function(){
            options.onScroll();
        });
    }
}

Nasze przesuwanie realizujemy tylko w momencie gdy nasza lista się nie przesuwa, stąd na początku sprawdzenie $ul.not(':animated').

Na początku tej metody po raz kolejny pobieram wszystkie LI naszej listy. Czemu?
Pobrane na początku pluginu LI podstawione są już pod zmienną, dlatego nie odzwierciedlają przerzucania z końca na początek i początku na koniec. Stąd musimy za każdym razem pobrać nasze li na nowo - lokalnie.

Pozostał nam przeciwny kierunek:

  • Przesuwamy naszą listę w lewo (0-item_width)
  • Po animacji wstawiamy po końcowym LI ten z początku
  • ustawiamy pozycję UL na 0

var scrollNext = function() {
    var $li = $ul.find('li');
    $ul.not(':animated').animate({'margin-left' : -itemWidth}, options.animationTime, function(){
        $li.last().after($li.first());
        $ul.css({'margin-left' : 0});
        options.onScroll();
    });
}

Nasz plugin ma teraz postać:


(function($){
    $.fn.carousel = function(options) {
        options = $.extend({
            displayLi      : 3,
            animateTime   : 700,
            pauseTime     : 3000,
            onScroll        : function() {}
        }, options);

        return this.each(function() {
            var $t = $(this);

            var $ul = $t.find('ul');
            var $li = $ul.find('li');
            var item_width = $li.first().outerWidth(true);
            var $prev = $t.find('.prev');
            var $next = $t.find('.next');
            var interval = null;

            var scrollPrev = function() {
                if (!$ul.is(':animated')) {
                    var $li = $ul.find('li');
                    $ul.css('margin-left',-item_width);
                    $li.first().before($li.last());
                    $ul.animate({'margin-left' : 0}, options.animationTime, function(){
                        options.onScroll();
                    });
                }
            }

            var scrollNext = function() {
                var $li = $ul.find('li');

                $ul.not(':animated').animate({'margin-left' : -item_width}, options.animationTime, function(){
                    $li.last().after($li.first());
                    $ul.css({'margin-left' : 0});
                    options.onScroll();
                });
            }                

            $prev.bind('click', scrollPrev);
            $next.bind('click', scrollNext);
        });
    }
})(jQuery);

Poproszę automatycznie

I tak doszliśmy do końca pisania pluginu. Wszystko zostało powiedziane, złe charaktery pokonane. W zasadzie pozostało nam dorzucenie do naszego dzieła... automatyzacji. A tak - samotna zmienna interval nigdy nie została użyta. Aby nasz slider dodatkowo automatycznie się przesuwał, wykorzystamy dobrze nam znaną metodę setTimeout(), którą podstawimy pod naszą samotną zmienną. Na końcu naszego pluginu dopisujemy więc magiczną linijkę, która po określonym czasie (podanym w parametrach) odpali przesunięcie:


    if (options.auto) interval = setTimeout(function() {scrollNext()}, options.pauseTime);

Jak zauważyłeś, dodaliśmy także dodatkową opcję w optionsach naszego pluginu. Nie zawsze będziemy chcieli automatycznego działania naszej karuzeli, dlatego zostawiamy sobie możliwość zmiany tej wartości:


options = $.extend({
    displayLi      : 3,
    animateTime    : 700,
    pauseTime      : 3000,
    auto           : false,
    onScroll       : function() {}
}, options);

Automatyczne przesuwanie powinno być dodatkowo odpalane powtarzalnie, tak więc podobny kod musimy wstawić na końcu funkcji scrollNext i scrollPrev. I w zasadzie to by wystarczyło, gdyby nie pewna ułomność tego rozwiązania. Podczas ręcznego przesuwania listy, nasz interwał działa równocześnie. Aby pozbyć się tego mankamentu, wystarczy przy każdym odpaleniu tych metod ustawiać czas do automatycznego przejścia na nowo (czyli wpierw czyścić poprzednio ustawiony):


var scrollPrev = function() {
    //...
    clearInterval(interval);
    if (options.auto) {
        interval = setTimeout(function() {
            scrollNext()
        }, options.pauseTime);
    }
}

var scrollNext = function() {
    //...
    clearInterval(interval);
    if (options.auto) {
        interval = setTimeout(function() {
            scrollNext()
        }, options.pauseTime);
    }
}

Finito! Nasz totalnie gotowy w pełni automatyczny plugin ma postać:


(function($){
    $.fn.carousel = function(options) {
        options = $.extend({
            displayLi      : 3,
            animationTime  : 700,
            pauseTime      : 3000,
            auto            : false,
            onScroll        : function() {}
        }, options);

        return this.each(function() {
            var $t = $(this);

            var $ul = $t.find('ul');
            var $li = $ul.find('li');
            var item_width = $li.first().outerWidth(true);
            var $prev = $t.find('.prev');
            var $next = $t.find('.next');
            var interval = null;

            var scrollPrev = function()
            {
                if (!$ul.is(':animated')) {
                    var $li = $ul.find('li');
                    $ul.css('margin-left',-item_width);
                    $li.first().before($li.last());
                    $ul.animate({'margin-left' : 0}, options.animationTime, function(){
                        options.onScroll();
                    });
                    clearInterval(interval);
                    if (options.auto) {
                        interval = setTimeout(function() {
                            scrollNext()
                        }, options.pauseTime);
                    }
                }
            }

            var scrollNext = function()
            {
                var $li = $ul.find('li');

                $ul.not(':animated').animate({'margin-left' : -item_width}, options.animationTime, function(){
                    $li.last().after($li.first());
                    $ul.css({'margin-left' : 0});
                    options.onScroll();
                });
                clearInterval(interval);
                if (options.auto) {
                    interval = setTimeout(function() {
                        scrollNext()
                    }, options.pauseTime);
                }
            }                

            $prev.bind('click', scrollPrev);
            $next.bind('click', scrollNext);

            if (options.auto) interval = setTimeout(function() {scrollNext()}, options.pauseTime);
        });
    }
})(jQuery);

Wystarczy teraz odpalić nasze dzieło i cieszyć się z wykonania dobrej roboty:


$(function(){
    $('.box').carousel({
        displayLi     : 3,
        animateTime   : 700,
        pauseTime     : 3000,
        auto           : true,
        onScroll       : function() {}
    });
});