WTF, forms?

Jakiś czas temu pojawił się w sieci artykuł o zawrotnym tytule: WTF, forms?.
Tak się szczęśliwie składa, że też zajmowałem się ostatnio tym tematem.
Poniższy tekst będzie w pewnej części kopią tamtego artykułu. Ale tylko w części. Reszta będzie super oryginalna i najwspanialsza na świecie (standardowo).

„Co do cholery? formularze” chciało by się powiedzieć. Już od dawna wiadomo, że są na tym świecie rzeczy nie do ruszenia. Wśród nich są także webowe formularze. Już raz zajmowaliśmy się zmienianiem wyglądu kontrolek formularzy. Nie będę ukrywał, że stosując tamte skryptowe rozwiązania miałem z nimi nie raz problemy.

Checkboxy i radio

Zmieniając wygląd checkboxa zastosujemy kod:

<label class="checkbox-cnt">
    <input type="checkbox">
    <i class="state"></i>
    <span>Przykładowy tekst checkboxa</span>
</label>

W powyższym kodzie .state to „ptaszek checkboxa”. Tekst checkboxa okryliśmy dodatkowym spanem by mieć nad nim jeszcze większą kontrolę (np. zmienić mu opacity).
Za pomocą selektora input:checked sprawdzimy, czy dany input jest zaznaczony. Za pomocą selektora ~ pobierzemy element leżący za inputem (to samo możemy osiągnąć przez użycie selektora +).

.checkbox-cnt {}
.checkbox-cnt input {}

/* ptaszek i tekst checkboxa */
.checkbox-cnt input ~ .state {}
.checkbox-cnt input ~ span {}

/* gdy zaznaczony */
.checkbox-cnt input:checked ~ .state {}
.checkbox-cnt input:checked ~ span {}

/* z plusem wyglądało by to mniej wiecej tak */
.checkbox-cnt input:checked + .state {}
.checkbox-cnt input:checked + .state + span {}

Input ukrywamy za pomocą opacity:0.
Grafikę „ptaszka” checkboxa możemy uzyskać na kilka sposobów. Najprostszym jest dodanie tła dla elementu .state. Tego opisywać nie będziemy. Możemy też wykorzystać pseudo selektory :before lub :after za pomocą których wstawimy znak strzałki z graficznego fonta:

.checkbox-cnt input {
    position:absolute;
    top:0; left:0;
    width:25px;
    height:25px;
    z-index: 2;
    cursor:pointer;
    padding:0;
    margin:0;
    opacity: 0;
}
.checkbox-cnt .state {
    position:realtive;
    width:30px;
    height:30px;
}
.checkbox-cnt .state:before {
    height:30px;
    width:30px;
    line-height:30px;
    text-align:center;    
    display: block;
    content:'\f078';    
    -webkit-transition: 0.3s background;
    -moz-transition: 0.3s background;
    -o-transition: 0.3s background;
    -ms-transition: 0.3s background;
    transition: 0.3s background;
    position: absolute;
    top:11px;
    left:5px;
}
.checkbox-cnt input:checked ~ .state {
    ...
}
.checkbox-cnt input:checked ~ .state:before {
    ...
}

Możemy też wykorzystać :before i :after do wstawienia dwóch kresek, które odpowiednio obrócone (transform) stworzą ptaszka:

.checkbox-cnt .state:before {
    width:7px;
    height:3px;
    display: block;
    content:'';
    -moz-transform:rotate(45deg);
    -webkit-transform:rotate(45deg);
    -ms-transform: rotate(45deg);
    transform:rotate(45deg);
    -webkit-transition: 0.3s background;
    -moz-transition: 0.3s background;
    -o-transition: 0.3s background;
    -ms-transition: 0.3s background;
    transition: 0.3s background;
    position: absolute;
    top:11px;
    left:5px;
}
.checkbox-cnt .state:after {
    width:10px;
    height:3px;
    display: block;
    content:'';
    -moz-transform:rotate(-45deg);
    -webkit-transform:rotate(-45deg);
    -ms-transform:rotate(-45deg);
    transform:rotate(-45deg);    
    position: absolute;
    top:10px;
    left:8px;
}

Poniższy przykład pokazuje działanie tej metody. Dzięki takiemu podejściu, nie tylko możemy potem zmieniać kolor strzałki, ale nawet animować jej wygląd po najechaniu.

Powyższy przykład jest ciut rozbudowany, ale tak naprawdę wcale taki być nie musi. Kluczowe tutaj jest wykorzystanie selektorów + lub ~.

<input type="checkbox" />
    <label>Lorem ipsum</label>

W powyższym przykładzie wystarczy zbadać stan checkboxa i odpowiednio zmieniać wygląd labela

input[type=checkbox] + label {
        background:url(chkOFF.png) no-repeat;
        padding-left:30px;
    }
    input[type=checkbox]:checked + label {
        background:url(chkON.png) no-repeat;
    }

Select

Aby zmienić wygląd selekta, przede wszystkim musimy ukryć jego domyślną strzałkę. Teoretycznie najprostszym sposobem jej ukrycia jest użycie właściwości apperance:none. Niestety nie w każdej przeglądarce działa to jak powinno, dlatego skorzystamy z przepisu ze strony https://gist.github.com/joaocunha/6273016. Dodatkowo dla IE skorzystamy tutaj z ms-expand.

select { 
    text-indent: 0.01px;
    text-overflow: ""    
    -webkit-appearance: none; 
    -moz-appearance: none; 
    apperance:none;

    ...dodatkowe stylowanie selekta...
}
select::-ms-expand {
    display: none;
}

Teoretycznie powinniśmy mieć już sprawę załatwioną. Wystarczy więc dodać dodatkowe stylowanie do naszego selekta, użyć tła symulującego strzałkę i włala.

select {
    -webkit-appearance: none; 
    -moz-appearance: none; 
    apperance:none;        
    text-indent: 0.01px;
    text-overflow: "";

    background: #fff;
    width:100%;
    border:1px solid #CBD5DD;
    border-radius:2px;
    padding:10px 40px 10px 10px;
    box-shadow:inset 0 1px 3px rgba(0,0,0,0.1);
    -moz-box-sizing:border-box;
    -webkit-box-sizing:border-box;
    box-sizing:border-box;
    cursor: pointer;                    
    background:url(select-arrow.png) right center no-repeat;        
}
select::-ms-expand {
    display: none;
}

Niestety metoda ta ani nie zadziała na staszych IE (z ie9 włącznie), ani na najnowszym Firefox (v30), ponieważ ten ma błąd związany ze złą obsługą apperance:none;. Pal licho stare IE, ale czemu ognisty lisku robisz nam takie rzeczy…

Możemy albo to przemilczeć, albo skorzystać z bardzo, bardzo starej sztuczki. Ustawiamy selektowi width:115% i wrzucamy go do kontenera, który ma overflow:hidden. Dzięki temu strzałka selekta zostaje „wypchnięta” w prawą stronę w niewyświetlaną zawartość niebytu – czyli zostaje ukryta. Takiemu selektowi dodatkowo ustawiamy przezroczyste tło oraz usuwamy border. Przedwojenne Opery nie dawały się omamić tym trikiem, bo nie dało się w nich ustawiać przezroczystego tła dla selektów. W dzisiejszych czasach nie ma z tym problemu.

.select-cnt {
    height:40px;
    overflow:hidden;        
    position:relative;

    /* poniżej style wyglądu selekta */
    background: #fff;        
    box-shadow:inset 0 1px 3px rgba(0,0,0,0.1);
    border:1px solid #CBD5DD;
    border-radius:2px;        
}
.select-cnt select {
    background:none;
    border:0;
    width:115%;

    height:40px;
    padding:10px;
}
<div class="select-cnt">
    <select>
        <option>...</option>
    </select>
</div>

Gdy ukryjemy domyślną strzałkę, musimy dodać naszą własną. Tak samo jak w przypadku checkboxów możemy to zrobić na różne sposoby.
Możemy więc dodać zwyczajne tło z grafiką strzałki (najprostrze), możemy też tak jak poprzednio użyć :before i :after.

.select-cnt:before {
    width:12px;
    height:2px;
    border-radius:1px;
    display: block;
    content:'';
    background: #CBD5DD;
    -moz-transform:rotate(44deg);
    -webkit-transform:rotate(44deg);
    transform:rotate(44deg);
    color:#CBD5DD;
    -webkit-transition: 0.3s color; 
    -moz-transition: 0.3s color; 
    -o-transition: 0.3s color;  
    -ms-transition: 0.3s color; 
    transition: 0.3s color;
    position: absolute;
    top:19px;
    right:20px;
    z-index:1;
    pointer-events:none;
}   
.select-cnt:after {
    width:12px;
    height:2px;
    border-radius:1px;
    display: block;
    content:'';
    background: #CBD5DD;
    -moz-transform:rotate(-44deg);
    -webkit-transform:rotate(-44deg);
    transform:rotate(-44deg);
    color:#CBD5DD;
    -webkit-transition: 0.3s color; 
    -moz-transition: 0.3s color; 
    -o-transition: 0.3s color;  
    -ms-transition: 0.3s color; 
    transition: 0.3s color;
    position: absolute;
    top:19px;
    right:12px;
    z-index:1;
    pointer-events:none;
}

Jeżeli nasz selekt będzie miał w przyszłości jakieś stany (np. disabled), wtedy cały wygląd z .select-cnt warto przenieść na dodatkowy element leżący tuż za selektem (metoda działania ta sama jak w przypadku checboxów):

<div class="select-cnt">
    <select>
        <option>...</option>
    </select>
    <i class="select"></i>
</div>

W poniższym przykładzie pokazałem warianty opisanych powyżej sposobów:

Oglądając powyższe przykłady na staszych IE zauważysz, że grafika strzałki jest za duża. To dlatego, że w trosce o wyświetlacze Retina użyłem większej grafiki którą skaluję do odpowiednich rozmiarów za pomocą background-size. Jeżeli lubisz mieć nieostre strzałki na urządzeniach mobilnych, możesz użyć normalnej wielkości grafiki. Oczywiście i ten przypadek spokojnie można poprawić. Wystaczy dodać wyjątek dla starszych ie i ustawiać w nim alternatywne tło.

A co ze zmianą rozwijanej listy?
Do takich rzeczy warto użyć plugin http://github.hubspot.com/select/docs/welcome/ albo np. http://www.bulgaria-web-developers.com/projects/javascript/selectbox/. Ogólnie jednak jeżeli nie stosujesz takiego selekta jako pojedynczego prostego menu, lepiej powstrzymaj swoje zapędy w stylowaniu listy. Czemu? Pierwsza sprawa to dość problematyczna obsługa takich rzeczy na telefonach. Raz takie skrypty zadziałają, raz nie, nigdy nic nie wiadomo. Druga sprawa jest taka, że praktycznie zawsze takie stylowanie sprawia problemy. Czy rozwinięta lista powinna się pojawić pod selektem, czy nad (gdy jest na skraju ekranu)? Czy można ją obsługiwać za pomocą klawiatury? Czy jest odpowiednio widoczna na małych ekranach? Czy jeżeli lista ma dużo elementów to pojawi się pasek przewijania i czy będzie on widoczny na telefonach (które domyślnie przecież nie pokazują pasków). Czy w końcu taka lista będzie dobrze działać z selektami które są wrzucone do skrolowanego kontenera, który ma overflow:auto :) (tego ostatniego przypadku chyba żaden plugin nie ogarnie…).

File

W oryginalnym artykule WTF, forms? wynik dla file zwyczajnie mi się nie spodobał.
Gdy wybierzemy plik, nic nas nie informuje, że cokolwiek zrobiliśmy. Niestety w tym przypadku będziemy musieli skorzystać z prostego skryptu js, który poza podpięciem kliknięcia, okryje :file odpowiednimi divami. Końcowy kod po przemieleniu przez JS zamieni :file na coś takiego:

<div class="file-cnt">
    <div class="input-file">
        <span>Wybierz</span>
        <input type="file" title="Wybierz" style="font-size: 1000px; opacity: 0;" class="styled">
    </div>
    <span class="text"></span>
</div>

Najważniejszy tutaj jest ukryty :file który dzięki nadaniu dużej czcionki zajmuje całą powierzchnię .input-file (który to element symuluje zmieniony przycisk). Stylowanie dla tego elementu ma postać:

.file-cnt {
    vertical-align: top;
    width: 100%;
    height: 40px;
    display: block;
    position: relative;
    overflow: hidden;
}
.file-cnt .input-file {
    position:absolute;
    top:0;
    right:0;
    z-index:1;
    font-family:sans-serif;
    line-height:40px;
    text-align:center;
    color:#fff;
    height: 40px;
    width: 90px;     
    border-radius:0 3px 3px 0;
    background: #499bea;
    background: -moz-linear-gradient(top,  #499bea 0%, #0d5cdb 100%);
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#499bea), color-stop(100%,#0d5cdb));
    background: -webkit-linear-gradient(top,  #499bea 0%,#0d5cdb 100%);
    background: -o-linear-gradient(top,  #499bea 0%,#0d5cdb 100%);
    background: -ms-linear-gradient(top,  #499bea 0%,#0d5cdb 100%);
    background: linear-gradient(to bottom,  #499bea 0%,#0d5cdb 100%);
    box-shadow:-3px 0 3px rgba(0,0,0,0.2);
    
}
.file-cnt .input-file span {
    display:block;
    position:absolute;
    right:0;
    top:0;
    font-size:13px;
    width: 90px;
    height:40px;
    line-height:40px;
    text-align:center;
}
.file-cnt input[type="file"] {
    position: absolute;
    right: 0; top: 0;
    opacity: 0; filter: alpha(opacity=0);
    height: 100%;
    display: block;
    cursor: pointer;
    z-index:2;
}
.file-cnt .text {
    border-radius:2px;
    margin-right: 90px;
    height: 40px;    
    font-family:sans-serif;
    line-height: 40px;
    display: block;
    position: relative;
    padding: 0 15px;
    border: 1px solid #CBD5DD;
    border-right:0;
    background: #fff;
    box-shadow:inset 0 1px 3px rgba(0,0,0,0.1);
    border-top-right-radius:0;
    border-bottom-right-radius:0; 
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}
/*disabled */
.file-cnt.disabled, .file-cnt.disabled * {
    cursor:default !important;        
}
.file-cnt.disabled .input-file {
    border-left:1px solid #000 !important;
}
@media only screen and (max-width: 320px) {
    .file-cnt input[type="text"] {display: none;}
    .file-cnt .input-file {
        width: 100%;
        margin: 0;
    }
}

Aby zamienić :file na powyższy element, wystarczy skorzystać z poniższego bardzo prostego kodu:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script>
$.fn.styleFile = function(options) {
    options = $.extend({
        btnClass : ''
    }, options);

    return this.each(function() {
        styleFile = function($element){
            var classList = '';
            if ($element.attr('class') != undefined) {
                classList = $element.attr('class');
                $element.attr('class', '');
            }

            var labelText = ($element.attr('title')!='' && $element.attr('title')!=undefined)? $element.attr('title'):'Wybierz';
            var $container = $('<div class="file-cnt" />');
            var $textFile = $('<span class="text">'+$element.val()+'</span>');
            var $inputFileWrapper = $('<div class="input-file '+options.btnClass+'"><span>'+labelText+'</span></div>');

            $container.append($inputFileWrapper).append($textFile);
            $element.replaceWith($container);
            $container.find('.input-file').append($element);
            $element.css({'font-size' : 1000, opacity:0});

            $element.on('change click', function() {
                var $this = $(this);
                $this.parent().parent().find('.text').text($this.val());
            });

            var oldClassList = classList.split(' ');
            $.each(oldClassList, function(k) {
                $container.addClass(oldClassList[k]);
            });            
            if ($element.prop('disabled')) {
                $container.addClass('disabled');
            }
            $element.addClass('styled');
        };        

        var disableStyledElement = function() {
            var $this = $(this);
            var $parent = $this.closest('.file-container')
            $this.attr('disabled', 'disabled');
            if ($parent.hasClass('styled')) $parent.addClass('disabled');                
        };

        var enableStyledElement = function() {
            var $this = $(this);
            var $parent = $this.closest('.file-cnt')
            $this.removeAttr('disabled');
            $parent.removeClass('disabled');
        };

        var $this = $(this);
        
        if ($this.is(':file')) styleFile($this);

        $this.on({
            'disable' : disableStyledElement,
            'enable' : enableStyledElement
        })

    });    
}; /* styleFormElements */    
    
$(function() {
    $(':file').styleFile();
})
</script>

Na początku pobieramy klasy, które :file może przecież mieć przypisane. Po pobraniu przeniesiemy je do .file-cnt. Następnie tworzymy elementy, którymi okryjemy :file. Musimy jeszcze podpiać zdarzenie click. Po wybraniu pliku, wartość :file pobieramy i przenosimy do elementu .text.
Dodatkowo na końcu dodajemy do naszego nowo utworzonego elementu dwie metody: enable i disable, które w przyszłości w razie czego włączą lub wyłączą nasz element. Element :file jest teraz ukryty (opacity:0), więc dodanie mu właściwości :disabled nie zmieni wyglądu naszej kontrolki. Dlatego właścnie aby wyłaczyć naszą kontrolkę będziemy musieli skorzytać z kodu:

$('#superStyledInput').trigger('enable'); //włączenie wyłączonego :file
$('#superStyledInput').trigger('disable'); //wyłączenie włączonego :file

Komentarze