ScrollBar w JavaScript-cie z wykorzystaniem JQuery UI Slidera

Zrobienie swojego ładnie ostylowanego scrollbara, który będzie przesuwał część lub całą naszą stronę nie jest zbyt wymagającym zdaniem, zwłaszcza, że istnieje już mnóstwo gotowych skryptów. Jednak jak się ostatnio przekonałem, większość skryptów ma licencje do wykorzystywania do celów prywatnych, a jeśli nie to może się zdarzyć, że nie obsługują np. dynamicznej zmiany zawartości. Wówczas pozostaje zbudować scrollbar samemu... Jeżeli jednak korzystamy z frameworku JQuery to mamy możliwość wykorzystania gotowego komponentu z grupy JQuery UI - Slider.



Wówczas mamy już gotowy suwak, a pozostaje dorobić:
  • skórkę (najlepiej jak wymaga tylko zaokrąglenia) 
  • przesuwanie contentu
  • strzałki (prawo - lewo, góra - dół)
  • szerokości uchwytu
  • dostosowywania się do zmieniającego się contentu
Przykładowa grafika dla scrollbara wygląda mniej więcej tak:
Podstawowa struktura dla JQuery UI Slider: Struktura HTML - przed wygenerowaniem:
Generowanie:
$('#ruler').slider({min:0, max:100, orientation: 'vertical'});
Struktura HTML - po wygenerowaniu:
Po wygenerowaniu nie wczytując domyślnych styli, nasz obiekt pozostanie przezroczysty, wobec tego zaczniemy od podstawowego skórkowania.
    Skórka
    Do ostylowania scrollbara posłuży nam zdefiniowanie klas ui-slider oraz ui-slider-handle. Pierwsze służy do definicji tła, drugie uchwytu. W naszym przypadku definicje będą następujące:
    .ui-slider { 
     height:440px;
     position:relative;
     width:13px;
     margin: 10px 0px;
     border: solid 1px rgb(165,158,121);
    }
    
    .ui-slider-handle {
     background: rgb(197,197,168);
     width:100%; 
     height:20px;
     display:block;
     position:absolute;
     margin-bottom:-10px;
    }


    Wsadzanie różnych obrazków, spanów, divów itd. w .ui-slider-handle na ogół powoduje błędną obsługę drag'a wobec tego rogi zaokrąglimy dodatkiem do JQuery - Corners. Po testach okazuje się, że działa w każdej przeglądarce (IE 6-8, FF, Opera, Chrome, Safari).

    $('.ui-slider-handle').corner('2px');

    Przesuwanie contentu

    Głównym zadaniem scrollbara jest przesuwanie contentu. W tym celu zdefiniujemy zdarzenia slide oraz change dla elementu slider. Można to robić zarówno w konstruktorze lub już po jego wykonaniu. W przypadku JQuery 1.2.6 oraz JQuery UI 1.6 niestety zaobserwowałem, że podanie zdarzeń w konstruktorze nie działa :( Zatem zastosujemy drugą, dotychczas niezawodną metodę - bind.

    $('#ruler').bind('slide', moveContent);
    $('#ruler').bind('change', moveContent);
    
    Sama funkcja moveContent realizuje przesunięcie przesuwając warstwę .relative w pojemniku #mainContent. Właściwość top jest obliczana jako wartość suwaka (od 0 do 100) przemnożona przez różnicę wysokości warstwy przesuwanej i warstwy #mainContent :) i to wszystko pomnożone przez -1.
    function moveContent(e, ui) {
        var v;
        if (typeof ui != 'undefined' && ui != null)
            v = 100-ui.value;
        else if (typeof $('#ruler').data('slider') != 'undefined')
            v = 100-$('#ruler').data('slider').value();
        var w = $("#mainContent > .relative").height() - $("#mainContent").height();
        $('#mainContent > .relative').css('top', (-v * w /100) + 'px');
    }
    

    Należy pamiętać, że blok, który będziemy przesuwać należy umieścić w pojemniku (div'ie), który będzie miał ustawione overflow:hidden;



    Któż zbadał puszcz litewskich przepastne krainy
    Aż do samego środka, do jądra gęstwiny?
    Rybak ledwie u brzegów nawiedza dno morza;
    Myśliwiec krąży koło puszcz litewskich łoża,
    Zna je ledwie po wierzchu, ich postać, ich lice,
    Lecz obce mu ich wnętrzne serca tajemnice;
    Wieść tylko albo bajka wie, co się w nich dzieje.
    Bo gdybyś przeszedł bory i podszyte knieje,
    Trafisz w głębi na wielki wał pniów, kłod, korzeni,
    Obronny trzęsawicą, tysiącem strumieni
    I siecią zielsk zarosłych, i kopcami mrowisk,
    Gniazdami os, szerszeniów, kłębami wężowisk.

    Gdybyś i te zapory zmógł nadludzkiem męstwem,
    Dalej spotkać się z większem masz niebezpieczeństwem:
    Dalej co krok czyhają, niby wilcze doły,
    Małe jeziorka trawą zarosłe na poły,
    Tak głębokie, że ludzie dna ich nie dośledzą
    (Wielkie jest podobieństwo, że diabły tam siedzą).
    500 Woda tych studni sklni się, plamista rdzą krwawą.
    A z wnętrza ciągle dymi, zionąc woń plugawą,
    Od której drzewa wkoło tracą liść i korę;
    Łyse, skarłowaciałe, robaczliwe, chore,
    Pochyliwszy konary mchem kołtunowate
    I pnie garbiąc brzydkiemi grzybami brodate,
    Siedzą wokoło wody jak czarownic kupa
    Grzejąca się nad kotłem, w którym warzą trupa.




    Strzałki


    Strzałki zaimplementujemy za pomocą sprite'ów CSS. Wycinamy co trzeba (nie koniecznie z zaokrąglonymi rogami) i definiujemy CSS'y:



    #arrowDown {
        width:13px;
        height:15px;
        display:block;
        background: rgb(242,242,235) url(../images/arrows.png) no-repeat -13px 0px;
    }
    
    #arrowUp {
        width:13px;
        height:15px;
        display:block;
        background: rgb(242,242,235) url(../images/arrows.png) no-repeat 0px 0px;
    }
    
    a#arrowDown:hover {
        background-position: -13px -15px; 
    }
    
    a#arrowUp:hover {
        background-position: 0px -15px;
    }
    

    Dorobienie zaokrągleń na rogach jest również możliwe poprzez bibliotekę JQuery Corner. Niestety IE nie lubi zaokrąglać borderów, więc konieczne jest zastosowanie warstwy zewnętrznej na #arrowUp i #arrowDown z kolorem ramki. Ponadto IE trochę słabiej renderuje zaokrąglenie ale zwiększenie promienia pozwala wyeliminować tą niedogodność:
    .c {
        background: rgb(165,158,121);
    }
    
    if ($.browser.msie) {
       $('#arrowDown').corner('bottom 5px').parent().css('padding', '0px 1px 1px 1px').corner("bottom 5px");
       $('#arrowUp').corner('top 5px').parent().css('padding', '1px 1px 0px 1px').corner("top 5px");
    } else {
       $('#arrowDown').corner('bottom 3px').parent().css('padding', '0px 1px 1px 1px').corner("bottom 3px");
       $('#arrowUp').corner('top 3px').parent().css('padding', '1px 1px 0px 1px').corner("top 3px");
    }
    

    W przypadku, kiedy robimy scrollbar pionowy (tak jak w przykładzie) nie jest wymagane, aby stosować właściwość float:left; W przypadku poziomych będzie to koniecznością.
    Następnie zabieramy się za przypisanie zdarzeń do przycisków. Głównym założeniem będzie:
    • zdarzenie mousedown - uruchomienie przewijania
    • funkcja przewijania wpada w pętlę, która się kończy w momencie, kiedy zdarzenie mouseup ustawi odpowiednią flagę
    • zdarzenie mouseup - ustawia odpowiednią flagę
    Flaga będzie miała nazwę move i zostanie przypisana do obiektu #ruler
    $('#ruler').data('move', false);
    
    Funkcje odpowiadające za ruch zostały rozbite na dwie: move - odpowiada za ruch oraz stopMoving - która ustawia flagę move na false. Po obliczeniu wartości uruchamiana jest metoda moveContent jednak bez parametru, co powoduje, że aktualny stan suwaka jest pobierany bezpośrednio z obiektu #ruler
    function move(direction) {
        var obj = $('#arrow'+direction);
        var exp = 1;
        if (direction == 'Down')
            exp = -1;
        
        if ($('#ruler').data('move')) {
            var value = $('#ruler').data('slider').value() + 2*exp;
            $('#ruler').data('slider').value(value);
            moveContent();
            
            var timeout = setTimeout(function(){move(direction)},10);
            $('#ruler').data('moveTimeout', timeout);
        }    
    }
    
    function stopMoving() {
        $('#ruler').data('move', false);
        clearTimeout($('#ruler').data('moveTimeout'));
        $('#ruler').removeData('moveTimeout');
    }
    

    Funkcje te są podczepiane do zdarzeń:
    $('#arrowUp').mousedown(function() {
       $('#ruler').data('move', true);
       move('Up');
    });
    $('#arrowDown').mousedown(function() {
       $('#ruler').data('move', true);
       move('Down');
    });
    $('#arrowDown, #arrowUp').mouseup(stopMoving); 

    Problem z ramką, który występuje poprzez nadanie marginesów - górnego i dolnego został wyeliminowany poprzez utworzenie warstwy powyżej #rulerBorder, która przejmuje zarówno ramkę jak i kolor tła. Więcej w następnym rozdziale.





    Któż zbadał puszcz litewskich przepastne krainy
    Aż do samego środka, do jądra gęstwiny?
    Rybak ledwie u brzegów nawiedza dno morza;
    Myśliwiec krąży koło puszcz litewskich łoża,
    Zna je ledwie po wierzchu, ich postać, ich lice,
    Lecz obce mu ich wnętrzne serca tajemnice;
    Wieść tylko albo bajka wie, co się w nich dzieje.
    Bo gdybyś przeszedł bory i podszyte knieje,
    Trafisz w głębi na wielki wał pniów, kłod, korzeni,
    Obronny trzęsawicą, tysiącem strumieni
    I siecią zielsk zarosłych, i kopcami mrowisk,
    Gniazdami os, szerszeniów, kłębami wężowisk.

    Gdybyś i te zapory zmógł nadludzkiem męstwem,
    Dalej spotkać się z większem masz niebezpieczeństwem:
    Dalej co krok czyhają, niby wilcze doły,
    Małe jeziorka trawą zarosłe na poły,
    Tak głębokie, że ludzie dna ich nie dośledzą
    (Wielkie jest podobieństwo, że diabły tam siedzą).
    500 Woda tych studni sklni się, plamista rdzą krwawą.
    A z wnętrza ciągle dymi, zionąc woń plugawą,
    Od której drzewa wkoło tracą liść i korę;
    Łyse, skarłowaciałe, robaczliwe, chore,
    Pochyliwszy konary mchem kołtunowate
    I pnie garbiąc brzydkiemi grzybami brodate,
    Siedzą wokoło wody jak czarownic kupa
    Grzejąca się nad kotłem, w którym warzą trupa.




    Szerokość uchwytu


    Szerokość uchwytu jest zależna od stosunku szerokości contentu do szerokości rodzica contentu (czyli tego co jest widoczne). Oczywiście należy pamiętać o minimalnej szerokości. W tym miejscu też zajmiemy się ukrywaniem paska, jeśli szerokość contentu jest za mała.
    JQuery UI Slider zakłada, że zawsze będzie potrzeba wystawania paska poza obszar po którym się poruszamy więc przyjęto założenie, że do paska nadawany jest styl margin-bottom: -(pół wysokości .ui-slider-handle). Niestety w przypadku, kiedy chcemy, aby suwak nie wystawał nam poza określony obszar konieczne jest zdefiniowanie marginesów, które będą równe tyle samo margin-bottom w suwaku.
    To nieco pogarsza sytuację ponieważ konieczne jest dorobienie warstwy wyżej, która przejmie ramkę - w naszym przypadku to będzie #rulerBorder.
    #rulerBorder {
        border-left: solid 1px rgb(165,158,121);
        border-right: solid 1px rgb(165,158,121);
        background: rgb(242,242,235);
        overflow:hidden;
    }
    

    Szerokość uchwytu jest wyznaczona jako stosunek wysokości widocznej części do całej wysokości warstwy przesuwanej (pomnożone przez maksymalną wysokość scrollbara).
    Musimy też pamiętać o zmianie rozmiarów marginesów podczas zmiany wysokości scrollbara.
    function setHandleHeight() {
        var sliderHeight = $('.ui-slider').innerHeight();
        var oldMargin = parseInt($('.ui-slider').css('margin-top'));
        
        $('.ui-slider-handle').css('height', (($('#pojMainContent').height() / $('#pojMainContent > .relative').height()))*(sliderHeight+oldMargin*2) + 'px');
        var margin = $('.ui-slider-handle').height();
        $('.ui-slider-handle').css('margin-bottom', -margin/2);
        $('.ui-slider').css('margin-top', margin/2);
        $('.ui-slider').css('margin-bottom', margin/2);
        
        $('.ui-slider').css('height', sliderHeight + 2*oldMargin - margin);
    }
    

    W tym miejscu poruszę jeszcze jedną kwestię związaną z dodaniem marginesów. Sam obiekt Slider nie zakładał użycia marginesów podczas korzystania z niego - więc na tym obszarze nie są obsługiwane zdarzenia - takie jak click. Wobec tego konieczne jest dorobienie zdarzenia tak, aby zachowanie na całym obszarze było takie same - czyli suwak przesuwał się do miejsca gdzie zostało kliknięte (a dokładniej jego środek powinien się znaleźć w miejscu kliknięcia). Jako, że obszary z marginesem zajmują wielkość połowy wysokości handlera, to zadanie sprowadza się do wykrycia, który obszar został kliknięty i przesunięcia suwaka na samą górę lub sam dół:
    $('#scrollbar').click(function(e){
        var y = (e.originalEvent.layerY)?e.originalEvent.layerY:e.originalEvent.offsetY;
        var height = $('#ruler').height();
        if (y<$('.ui-slider-handle').height()/2)
           $('#ruler').data('slider').value(100);
        else if (y>height-$('.ui-slider-handle').height()/2)
           $('#ruler').data('slider').value(0);
        moveContent();
     }); 
    





    Któż zbadał puszcz litewskich przepastne krainy
    Aż do samego środka, do jądra gęstwiny?
    Rybak ledwie u brzegów nawiedza dno morza;
    Myśliwiec krąży koło puszcz litewskich łoża,
    Zna je ledwie po wierzchu, ich postać, ich lice,
    Lecz obce mu ich wnętrzne serca tajemnice;
    Wieść tylko albo bajka wie, co się w nich dzieje.
    Bo gdybyś przeszedł bory i podszyte knieje,
    Trafisz w głębi na wielki wał pniów, kłod, korzeni,
    Obronny trzęsawicą, tysiącem strumieni
    I siecią zielsk zarosłych, i kopcami mrowisk,
    Gniazdami os, szerszeniów, kłębami wężowisk.

    Gdybyś i te zapory zmógł nadludzkiem męstwem,
    Dalej spotkać się z większem masz niebezpieczeństwem:
    Dalej co krok czyhają, niby wilcze doły,
    Małe jeziorka trawą zarosłe na poły,
    Tak głębokie, że ludzie dna ich nie dośledzą
    (Wielkie jest podobieństwo, że diabły tam siedzą).
    500 Woda tych studni sklni się, plamista rdzą krwawą.
    A z wnętrza ciągle dymi, zionąc woń plugawą,
    Od której drzewa wkoło tracą liść i korę;
    Łyse, skarłowaciałe, robaczliwe, chore,
    Pochyliwszy konary mchem kołtunowate
    I pnie garbiąc brzydkiemi grzybami brodate,
    Siedzą wokoło wody jak czarownic kupa
    Grzejąca się nad kotłem, w którym warzą trupa.




    Dostosowywanie się do zmieniającego się contentu

    Zdarzenie resize w przypadku divów itp. nie zawsze działa tak jak powinno więc konieczne jest wywołanie go po dokonanych zmianach. W ramach funkcji obsługującej zmieniający się content konieczne jest wykrywanie czy scrollbar w ogóle jest jeszcze potrzebny, a jeśli jest to konieczne jest ustawienie na nowo jego szerokości oraz położenia :)
    $('#mainContent > .relative').resize(handleResize);
    
    function handleResize() {
       setHandleHeight();
       var val = (-parseInt($('#mainContent .relative').css('top')) / $('#mainContent .relative').height())*100;
       $('#ruler').slider('value', val);
    }
    




    Któż zbadał puszcz litewskich przepastne krainy
    Aż do samego środka, do jądra gęstwiny?
    Rybak ledwie u brzegów nawiedza dno morza;
    Myśliwiec krąży koło puszcz litewskich łoża,
    Zna je ledwie po wierzchu, ich postać, ich lice,
    Lecz obce mu ich wnętrzne serca tajemnice;
    Wieść tylko albo bajka wie, co się w nich dzieje.
    Bo gdybyś przeszedł bory i podszyte knieje,
    Trafisz w głębi na wielki wał pniów, kłod, korzeni,
    Obronny trzęsawicą, tysiącem strumieni
    I siecią zielsk zarosłych, i kopcami mrowisk,
    Gniazdami os, szerszeniów, kłębami wężowisk.

    Gdybyś i te zapory zmógł nadludzkiem męstwem,
    Dalej spotkać się z większem masz niebezpieczeństwem:
    Dalej co krok czyhają, niby wilcze doły,
    Małe jeziorka trawą zarosłe na poły,
    Tak głębokie, że ludzie dna ich nie dośledzą
    (Wielkie jest podobieństwo, że diabły tam siedzą).
    500 Woda tych studni sklni się, plamista rdzą krwawą.
    A z wnętrza ciągle dymi, zionąc woń plugawą,
    Od której drzewa wkoło tracą liść i korę;
    Łyse, skarłowaciałe, robaczliwe, chore,
    Pochyliwszy konary mchem kołtunowate
    I pnie garbiąc brzydkiemi grzybami brodate,
    Siedzą wokoło wody jak czarownic kupa
    Grzejąca się nad kotłem, w którym warzą trupa.


    Kopiuj wpis



    I tak oto mamy w pełni działający scrollbar w JavaScripcie. Poniżej pełny kod w JS oraz CSS.

    #scrollbar {
     width:15px;
     position:relative;
     z-index:333;
    }
    
    .ui-slider { 
     height:188px;
     position:relative;
     width:13px;
     margin: 10px 0px;
    }
    
    .ui-slider-handle {
     background: rgb(197,197,168);
     width:100%; 
     height:20px;
     display:block;
     position:absolute;
     margin-bottom:-10px;
    }
    
    .c {
     background: rgb(165,158,121);
    }
    
    #arrowDown {
     width:13px;
     height:15px;
     display:block;
     background: rgb(242,242,235) url(../images/arrows.png) no-repeat -13px 0px;
    }
    
    #arrowUp {
     width:13px;
     height:15px;
     display:block;
     background: rgb(242,242,235) url(../images/arrows.png) no-repeat 0px 0px;
    }
    
    a#arrowDown:hover {
     background-position: -13px -15px; 
    }
    
    a#arrowUp:hover {
     background-position: 0px -15px;
    }
    
    #rulerBorder {
     border-left: solid 1px rgb(165,158,121);
     border-right: solid 1px rgb(165,158,121);
     background: rgb(242,242,235);
     overflow:hidden;
    }
    

    $(document).ready(function() {
       if ($('#mainContent .relative').height() > $('#mainContent').height()) {
      $('#ruler').slider({
       min: 0, 
          max: 100,
          orientation:'vertical',
          value: 100
      });
      $('#ruler').bind('slide', moveContent);
      $('#ruler').bind('change', moveContent);
      
      $('#arrowUp, #arrowDown').click(function() {return false;});
      
      $('#arrowUp').mousedown(function() {
       $('#ruler').data('move', true);
       move('Up');
      });
      $('#arrowDown').mousedown(function() {
       $('#ruler').data('move', true);
       move('Down');
      });
      $('#arrowDown, #arrowUp').mouseup(stopMoving);
      $('#arrowDown').data('move', false);
      
      if ($.browser.msie) {
      
       $('#arrowDown').corner('bottom 5px').parent().css('padding', '0px 1px 1px 1px').corner("bottom 5px");
       $('#arrowUp').corner('top 5px').parent().css('padding', '1px 1px 0px 1px').corner("top 5px");
      } else {
       $('#arrowDown').corner('bottom 3px').parent().css('padding', '0px 1px 1px 1px').corner("bottom 3px");
       $('#arrowUp').corner('top 3px').parent().css('padding', '1px 1px 0px 1px').corner("top 3px");
      }
      $('#scrollbar').click(function(e){
       var y = (e.originalEvent.layerY)?e.originalEvent.layerY:e.originalEvent.offsetY;
       var height = $('#ruler').height();
       if (y<$('.ui-slider-handle').height()/2)
        $('#ruler').data('slider').value(100);
       else if (y>height-$('.ui-slider-handle').height()/2)
        $('#ruler').data('slider').value(0);
       moveContent();
       
      })
      setHandleHeight();
      
     }
     $('#mainContent > .relative').resize(handleResize);
    });
    
    function handleResize() {
        setHandleHeight();
        var val = 100-((-parseInt($('#mainContent .relative').css('top')) / $('#mainContent .relative').height())*100);
        $('#ruler').slider('value', val);
    }
    
    function setHandleHeight() {
     var sliderHeight = $('.ui-slider').innerHeight();
     var oldMargin = parseInt($('.ui-slider').css('margin-top'));
     
     $('.ui-slider-handle').css('height', (($('#mainContent').height() / $('#mainContent > .relative').height()))*(sliderHeight+oldMargin*2) + 'px');
     var margin = $('.ui-slider-handle').height();
     $('.ui-slider-handle').css('margin-bottom', Math.floor(-margin/2));
     $('.ui-slider').css('margin-top', Math.floor(margin/2));
     $('.ui-slider').css('margin-bottom', Math.floor(margin/2));
     
     var newHeight = sliderHeight + 2*oldMargin - margin;
     var diff = (sliderHeight + oldMargin*2) - ((Math.floor(margin/2) * 2) + newHeight);
     
     $('.ui-slider').css('height', newHeight + diff);
    }
    
    function move(direction) {
     var obj = $('#arrow'+direction);
     var exp = 1;
     if (direction == 'Down')
      exp = -1;
     
     if ($('#ruler').data('move')) {
      var value = $('#ruler').data('slider').value() + 2*exp;
      $('#ruler').data('slider').value(value);
      moveContent();
      
      var timeout = setTimeout(function(){move(direction)},10);
      $('#ruler').data('moveTimeout', timeout);
     } 
    }
    
    function stopMoving() {
     $('#ruler').data('move', false);
     clearTimeout($('#ruler').data('moveTimeout'));
     $('#ruler').removeData('moveTimeout');
    }
    
    function moveContent(e, ui) {
     var v;
     if (typeof ui != 'undefined' && ui != null)
      v = 100-ui.value;
     else if (typeof $('#ruler').data('slider') != null)
      v = 100-$('#ruler').data('slider').value();
     var w = $("#mainContent > .relative").height() - $("#mainContent").height();
     $('#mainContent > .relative').css('top', (-v * w /100) + 'px');
    
    }
    

    Prześlij dalej:

    7 komentarzy:

    Serchio pisze...

    Witaj,

    Dobry tutorial, jednak pogubiłem się gdzieś w połowie. Istnieje możliwość, abyś umieścił kod html strony zawierającej slider?

    Michał B. pisze...

    Witaj,

    wrzuciłem przykładowy kod na Google Code:
    http://code.google.com/p/michal-javascript-examples/downloads/list

    Pozdrawiam,

    Anonimowy pisze...

    Dobra robota.
    pozdrawiam

    Anonimowy pisze...

    Fajna sprawa, a jak to ma się do wersji w poziomie? Co trzeba było by zrobić by mieć aktywny jeszcze scroll??

    Michał Biniek pisze...

    Co do poziomu to trzeba przerobić wyświetlanie całego scrollbara - poczynając od orientation na samym początku, przechodząc nieco przez css'y, aż do zmiany w js, gdzie konieczne jest przesuwanie left a nie top. Co do aktywnego scrolla to nie do końca rozumiem o co chodzi - w przykładzie jest zastosowany styl overflow:hidden - jeśli chcemy mieć scrolla wówczas powinniśmy zmienić na overflow:auto.

    Kamil pisze...

    Bardzo dziękuję za artykuł. Wiesz może w jaki sposób podłączyć jquery.mousewheel tak żeby w obrębie DIV'a do przewijania działała rolka myszy?

    Michal Biniek pisze...

    Na jsFiddle umieściłem przykład - wygląda, że rolka myszy działa także w obrębie diva.

    http://jsfiddle.net/SW5nG/

    Prześlij komentarz