Multiselekt wersja ukryta

Ostatnim razem stworzyliśmy nakładkę na multiselekta. Dostępne w internecie pluginy o podobnej funkcjonalności domyślnie ukrywają całą listę wyboru, a pokazują tylko pole, w które należy kliknąć by taką listę pokazać. My też przerobimy nasze ostatnie dzieło by zachowywało się podobnie do tamtych pluginów. Dzięki temu będziemy mieli dwie wersje – widoczną i niewidoczną po wejściu na stronę (do wyboru do koloru).

Zobacz demo

Tak naprawdę kod nie będzie się bardzo różnić od poprzedniej wersji. Dlatego skupimy się na różnicach, a nie ponownym omawianiu tych samych elementów. Jeżeli nie czytałeś poprzedniego artykułu, koniecznie musisz nadrobić braki.

Poniższa grafika demonstruje końcowy wygląd z zaznaczonymi elementami. Na czerwono zaznaczyłem nowe elementy:

multiselect-v2

Tak jak porzednio zacznijmy od końcowego kodu który wygenerujemy:

<div class="multiselect">

    <select id="xxx" multiple="multiple" style="display: none;">        
        <option value="">Przykładowa wartość 1</option>
        <option value="">Przykładowa wartość 2</option>
        ...
    </select>

    <input type="text" class="fake-multiselect" value="Wybierz" />

    <div class="multiselect-cnt">
        <div class="multiselect-controls">
            <div class="filter-cnt">
                <label for="xxx_filter">Szukaj:</label>
                <input type="text" placeholder="Szukaj" id="xxx_filter">
            </div>
            <div class="buttons-cnt">
                <a class="select-all" href="#">Zaznacz widoczne</a>
                <a class="deselect-all" href="#">Odznacz widoczne</a>
            </div>

            <a href="#" class="close" title="Zamknij">Zamknij</a>

        </div>

        <div class="list-container">
            <ul class="list">
                <li><input type="checkbox" id="_xxx0"><label for="_xxx0">Przykładowa wartość 1</label></li>
                <li><input type="checkbox" id="_xxx1"><label for="_xxx1">Przykładowa wartość 2</label></li>
                ...
            </ul>
        </div>
        
    </div>
</div>

Doszły nam dwa elementy. Input .fake-multiselect w który będziemy klikać by pokazać listę, oraz przycisk do zamykania listy (tą funkcjonalność podepniemy także pod input). W poprzedniej wersji na końcu kodu HTML był także element .multiselect-info ale go już nie potrzebujemy.

(function($) {     
    $.fn.multiSelectToCheckboxes = function(options) {
        options = $.extend({
            filterLabel : 'Szukaj:',
            filterPlaceholder : 'Szukaj',
            initPlaceholder : 'Wybierz',
            minFilterTextLength : 0,
            selectAllText : 'Zaznacz widoczne',
            deselectAllText : 'Odznacz widoczne',
            maxListToCount : 4, //po przekroczeniu tej wartosci pokaze ponizszy komunikat
            infoText : 'Wybrano {0} z {1}',
            showOnlyCountText : false, //czy pokazywac tylko powyzszy tekst w polu
            closeText : 'Zamknij'
        }, options);

        //...tutaj to co poprzednio...
        
        function createFakeInput($select, $ul) {
            //....
        }

        function updateFakeInput($select, $ul) { //poprzednio było to updateSelectInformation()
            //....
        } 

        return this.each(function() {
            var $select = $(this);
            var $wrapper = $('<div class="multiselect"></div>');
            $select.wrap($wrapper);

            var $ulContainer = $('<div class="list-container"></div>');
            var $ul = $('<ul class="list"></ul>');
            $ulContainer.append($ul);

            var $controls = createMultiselectControl($select, $ul);                
            
            var $container = $('<div class="multiselect-cnt"></div>');

            $container.append($controls).append($ulContainer);         
            
            var $input = createFakeInput($select, $ul);
            $select.parent().append($container).prepend($input);
            
            fillUl($select, $ul);
            updateFakeInput($select);                

            $select.hide();
            $select.on('update', function() {
                updateList($select, $ul)
            })
        }) 
    };
})(jQuery);

Jak widzisz doszło nam kilka nowych opcji, które wynikają z nowych elementów.
W głównej pętli (each) tworzymy wspomniany wcześniej „fejkowy” input, w który będziemy klikać by pokazać listę. W odróżnieniu od poprzedniej wersji informację o wybranych elementach listy nie będziemy umieszczać w osobnym divie (poprzednio było to pole .multiselect-info), a będziemy ją odpowiednio przygotowywać i wpisywać do fejkowego pola, które właśnie stworzyliśmy.

Tworzymy fejkowy input

W powyższym kodzie pojawiły dwie nowe funkcje: createFakeInput() oraz updateFakeInput() (która zastąpiła wcześniejszą updateSelectInformation()). Resztę kodu znamy, więc skupmy się na nich.

function createFakeInput($select, $ul) {
    var $input = $('');                    
    $input.prop('readonly', 1);
    $input.on('click', function() {
        $select.parent().toggleClass('active');
    })
    return $input;
}

Funkcja CreateFakeInput tworzy nam wspominany już fejkowy input, po kliknięciu którego będzeimy pokazywać resztę kotrolek multiselekta. Pokazanie takie wykonamy poprzed dodanie do wrapper (parrent()) klasę .active. Dzięki temu możemy potem zastosować stylowanie:

.multiselect .multiselect-cnt {
    display: none; /* domyślnie kontrolki są ukryte */
    border:1px solid #ddd; 
    padding:10px; 
    box-shadow:0 2px 2px -1px  rgba(0, 0, 0, 0.1)
}
.multiselect.active .multiselect-cnt {
    display: block;
}

Wypisujemy wybrane opcje

Kolejną funkcją jest updateSelectInformation() (w poprzedniej wersji była to funkcja updateSelectInformation()). Tym razem funkcja ta jest nieco dłuższa, ponieważ ma więcej opcji.

function updateFakeInput($select) {
    var $input = $select.parent().find('.fake-multiselect');                
    var $chk = $select.parent().find(':checkbox');
    var $chkSelected = $chk.filter(':checked');

    if ($chkSelected.length==0) {
        $input.val(options.initPlaceholder);
    } else {
        if (options.showOnlyCountText || ($chkSelected.length > options.maxListToCount)) {
            var tempArray = [$chkSelected.length, $chk.length];
            var text = options.infoText.replace(/{(\d+)}/g, function(match, number) { 
                return typeof tempArray[number] != 'undefined'? tempArray[number]: match
            });
            $input.val(text);
        } else {                        
            var labelsText = [];
            $chkSelected.each(function() {
                labelsText.push($(this).next('label').text());
            });
            $input.val(labelsText.join(','));      
        }                    
    }                
}

Nasz nowy plugin ma trzy opcje: initPlaceholder, maxListToCount i showOnlyCountText. Pierwsza z nich określa tekst, który wstawiamy w fejkowe pole, gdy żaden checkbox nie jest wybrany.

Po kliknięciu na jakiś checkbox do fejkowego pola wstawiany jest tekst który zawiera zaznaczone pole oddzielone przecinkiem. Generujemy go w liniach 16-20. Maksymalnie takich elementów może być wstawione tyle ule określa kolejna opcja maxListToCoun. Jeżeli liczba ta zostanie przekroczona, wtedy wstawiamy tekst generowany metodą poznaną w poprzedniej wersji („Wybrałeś {0} z {1}”).

Czasami jednak wcale nie będziemy chcieli wypisywać wybranych elementów po przecinku (szczególnie w przypadku, gdy ich nazwy będą bardzo długie i już w przypadku pojedyńczego wybranego elmentu jego tekst zajmie całą długość pola). Możemy to wyłączyć trzecią wspomnianą opcją czyli showOnlyCountText.

W zasadzie już skończyliśmy. Pozostała jeszcze mała pierdołka, ktorą warto dodać. Nasz fejkowy input w żaden sposób nie mówi użytkownikowi że po kliknięciu na niego reszta kontrolek zostanie ukryta. Dlatego właśnie w nagłówku kontrolek warto dodać dodatkowy przycisk zamykający. Nagłówek kontrolek generowała funkcja createMultiselectControl(), więc musimy do niej dodać nasz brakujący element:

function createMultiselectControl($select, $ul) {
    var id = $select.attr('id');
    var $headerContainer = $('<div class="multiselect-controls"></div>');

    //text filter container
    var $filterDiv = $('<div class="filter-cnt"></div>');                
    var $filterLabel = $('<label for="'+id+'_filter"">'+options.filterLabel+'</label>')
    var $filterInput = $('<input type="text" id="'+id+'_filter" placeholder="'+options.filterPlaceholder+'" />');
    $filterInput.on('keyup', function() {                
        var $li = $ul.children('li');
        var val = $(this).val();
        $ul.find('.empty-list').remove();

        if (val.length >= options.minFilterTextLength) {                        
            $ul.children('li').hide();
            $ul.children('li').filter(':contains("'+val+'")').show();
        }  
        
        if ($li.length) {
            if (!$li.filter(':visible').length) {
                $ul.parent().append('<strong class="empty-list">Brak wyników</strong>')
            }
        }
    });
    $filterDiv.append($filterLabel).append($filterInput);

    //select, unselect buttons
    var $buttonsDiv = $('<div class="buttons-cnt"></div>');
    var $buttonSelectAll = $('<a href="#" class="select-all">'+options.selectAllText+'</a>');
    var $buttonDeselectAll = $('<a href="#" class="deselect-all">'+options.deselectAllText+'</a>');
    $buttonSelectAll.on('click', function(e) {
        e.preventDefault();
        $ul.find('li:visible :checkbox').prop('checked', 1);
        $ul.find(':checkbox').trigger('change');
    })
    $buttonDeselectAll.on('click', function(e) {
        e.preventDefault();
        $ul.find('li:visible :checkbox').prop('checked', 0);
        $ul.find(':checkbox').trigger('change');
    })
    $buttonsDiv.append($buttonSelectAll).append($buttonDeselectAll);

    $headerContainer.append($filterDiv).append($buttonsDiv);

    //close button
    var $closeButton = $('<a href="#" class="close" title="Zamknij">'+options.closeText+'</a>');
    $closeButton.on('click', function(e) {
        e.preventDefault();
        $select.parent().toggleClass('active');
    })
    $headerContainer.append($closeButton);

    return $headerContainer;
}

Stylowanie takiego guziczka może mieć postać:

.multiselect .multiselect-controls .close {
    width:20%; 
    display: inline-block; 
    vertical-align: top; 
    width:22px; 
    height: 22px; 
    background:url(close.png) no-repeat; 
    overflow: hidden; 
    text-indent:-999px; 
    float:right;
    margin:3px 0 0 0;
 }

Cały kod

Cały kod naszego nowego pluginu wygląda teraz tak:

(function($) {     
    $.fn.multiSelectToCheckboxes = function(options) {
        options = $.extend({
            filterLabel : 'Szukaj:',
            filterPlaceholder : 'Szukaj',
            initPlaceholder : 'Wybierz',
            minFilterTextLength : 0,
            selectAllText : 'Zaznacz widoczne',
            deselectAllText : 'Odznacz widoczne',
            maxListToCount : 4, //po przekroczeniu tej wartosci pokaze ponizszy komunikat
            infoText : 'Wybrano {0} z {1}',
            showOnlyCountText : true, //czy pokazywac tylko powyzszy tekst w polu
            closeText : 'Zamknij'
        }, options);

        function fillUl($select, $ul) {                
            var baseId = "_" + $select.attr("id");
            $select.find("option, optgroup").each(function(index) {
                var $t = $(this);
                if ($t.is("option")) {
                    var id = baseId + index;
                    var $li = $("<li></li>").appendTo($ul);
                    var $checkbox = $("<input type='checkbox' id='" + id + "'/>")
                        .appendTo($li)
                        .on('change', function() {
                            var $chk = $(this)
                            if ($chk.is(":checked")) {
                                $chk.parent().addClass('select');
                                $t.attr("selected", "selected");                                    
                            } else {
                                $chk.parent().removeClass('select');
                                $t.removeAttr("selected");
                            }
                            updateFakeInput($select);
                        });
                    if ($t.is(":selected")) {
                        $checkbox.attr("checked", "checked");
                        $checkbox.parent().addClass('select');
                    }
                    $checkbox.after("<label for='" + id + "'>" + $t.text() + "</label>");
                } else {
                    var $li = $('<li class="group-name"><strong>' + $t.attr('label') + '</strong></li>').appendTo($ul);
                }
            });
            return $ul;
        };

        function updateList($select, $ul) {
            $ul.empty();
            fillUl($select, $ul);                
        };
     
        function createMultiselectControl($select, $ul) {
            var id = $select.attr('id');
            var $headerContainer = $('<div class="multiselect-controls"></div>');

            //text filter container
            var $filterDiv = $('<div class="filter-cnt"></div>');                
            var $filterLabel = $('<label for="'+id+'_filter"">'+options.filterLabel+'</label>')
            var $filterInput = $('<input type="text" id="'+id+'_filter" placeholder="'+options.filterPlaceholder+'" />');
            $filterInput.on('keyup', function() {                
                var $li = $ul.children('li');
                var val = $(this).val();
                $ul.find('.empty-list').remove();

                if (val.length >= options.minFilterTextLength) {                        
                    $ul.children('li').hide();
                    $ul.children('li').filter(':contains("'+val+'")').show();
                }  
                
                if ($li.length) {
                    if (!$li.filter(':visible').length) {
                        $ul.parent().append('<strong class="empty-list">Brak wyników</strong>')
                    }
                }
            });
            $filterDiv.append($filterLabel).append($filterInput);

            //select, unselect buttons
            var $buttonsDiv = $('<div class="buttons-cnt"></div>');
            var $buttonSelectAll = $('<a href="#" class="select-all">'+options.selectAllText+'</a>');
            var $buttonDeselectAll = $('<a href="#" class="deselect-all">'+options.deselectAllText+'</a>');
            $buttonSelectAll.on('click', function(e) {
                e.preventDefault();
                $ul.find('li:visible :checkbox').prop('checked', 1);
                $ul.find(':checkbox').trigger('change');
            })
            $buttonDeselectAll.on('click', function(e) {
                e.preventDefault();
                $ul.find('li:visible :checkbox').prop('checked', 0);
                $ul.find(':checkbox').trigger('change');
            })
            $buttonsDiv.append($buttonSelectAll).append($buttonDeselectAll);

            $headerContainer.append($filterDiv).append($buttonsDiv);

            //close button
            var $closeButton = $('<a href="#" class="close" title="Zamknij">'+options.closeText+'</a>');
            $closeButton.on('click', function(e) {
                e.preventDefault();
                $select.parent().toggleClass('active');
            })
            $headerContainer.append($closeButton);

            return $headerContainer;
        }            

        function updateFakeInput($select) {
            var $input = $select.parent().find('.fake-multiselect');                
            var $chk = $select.parent().find(':checkbox');
            var $chkSelected = $chk.filter(':checked');

            if ($chkSelected.length==0) {
                $input.val(options.initPlaceholder);
            } else {
                if (options.showOnlyCountText || ($chkSelected.length > options.maxListToCount)) {

                    var tempArray = [$chkSelected.length, $chk.length];
                    var text = options.infoText.replace(/{(\d+)}/g, function(match, number) { 
                        return typeof tempArray[number] != 'undefined'? tempArray[number]: match
                    });
                    $input.val(text);

                } else {                        
                    var labelsText = [];
                    $chkSelected.each(function() {
                        labelsText.push($(this).next('label').text());
                    })
                    $input.val(labelsText.join(','));
                
                }                    
            }                
        }

        function createFakeInput($select, $ul) {
            var $input = $('<input type="text" class="fake-multiselect" value="'+options.initPlaceholder+'" />');                    
            $input.prop('readonly', 1);
            $input.on('click', function() {
                $select.parent().toggleClass('active');
            })
            return $input;
        }

        return this.each(function() {
            var $select = $(this);
            var $wrapper = $('<div class="multiselect"></div>');
            $select.wrap($wrapper);

            var $ulContainer = $('<div class="list-container"></div>');
            var $ul = $('<ul class="list"></ul>');
            $ulContainer.append($ul);

            var $controls = createMultiselectControl($select, $ul);                            
            var $container = $('<div class="multiselect-cnt"></div>');
            $container.append($controls).append($ulContainer);         
            
            var $input = createFakeInput($select, $ul);
            $select.parent().append($container).prepend($input);
            
            fillUl($select, $ul);
            updateFakeInput($select);                

            $select.hide();
            $select.on('update', function() {
                updateList($select, $ul)
            })
        })       
    };
})(jQuery);

Oraz stylowanie:

.multiselect {
    width:600px;
}
/* kontrolki selekta */
    .multiselect .multiselect-controls {
        height:30px;
    }
    .multiselect .multiselect-cnt {
        display:none;
        border:1px solid #ddd;
        padding:10px;
        box-shadow:0 2px 2px -1px rgba(0, 0, 0, 0.1);
        margin-top:10px;
    }
    .multiselect.active .multiselect-cnt {
        display:block;        
    }
    .multiselect .filter-cnt {
        width:230px;
        margin-right:10px;
        display:inline-block;
        vertical-align:top;
    }
    .multiselect .filter-cnt label {
        display:none;
    }
    .multiselect .filter-cnt input {
        width:100%;
        display:block;
        height:30px;
        -webkit-box-sizing:border-box;
        -moz-box-sizing:border-box;
        box-sizing:border-box;
        border:1px solid #ddd;
        padding:0 10px 0 25px;
        border-radius:3px;
        background:url(search.png) 5px center no-repeat;
    }
    .ie .multiselect .filter-cnt input {
        line-height:30px;
    }
    .multiselect .filter-cnt input:focus,
    .multiselect .filter-cnt input:active {
        border:1px solid #bbb;
        background-color:#fafafa;
        box-shadow:inset 0 0 3px rgba(0, 0, 0, 0.2);
    }

/* guziki select-all i deselect-all */
    .multiselect .buttons-cnt {
        width:290px;
        display:inline-block;
        vertical-align:top;
        text-align:left;
    }
    .multiselect .buttons-cnt a {
        text-align:left;
        width:33%;
        display:inline-block;
        max-width:130px;
        padding-left:35px;
        height:30px;
        color:#444;
        font:11px/30px sans-serif;
        margin:0 2px;
        text-decoration:none;
        outline:none;
        border-radius:3px;
    }
    .multiselect .buttons-cnt a:hover {
        color:#000;
    }
    .multiselect .buttons-cnt a.select-all {
        background:url(ok.png) 10px center no-repeat;
    }
    .multiselect .buttons-cnt a.deselect-all {
        background:url(no.png) 10px center no-repeat;
    }
    .multiselect .multiselect-controls .close {
        width:20%;
        display:inline-block;
        vertical-align:top;
        width:22px;
        height:22px;
        background:url(close.png) no-repeat;
        overflow:hidden;
        text-indent:-999px;
        float:right;
        margin:3px 0 0 0;
    }

/* lista checkboxów */
    .multiselect .list-container {
        overflow:hidden;
        overflow-y:auto;
        height:209px;
        margin:10px 0 0 0;
        border:1px solid #fff;
        border-radius:3px;
        box-shadow:inset 0 0 6px rgba(0, 0, 0, .2);
        position:relative;
    }
    .multiselect .list-container .empty-list {
        font:bold 13px sans-serif;
        margin:90px 10px 0 10px;
        text-align:center;
        color:#444;
        display:block;
        padding:10px;
    }
    .multiselect ul {
        padding:0;
        margin:0;
        list-style:none;
        font:13px sans-serif;
    }
    .multiselect ul li {
        padding:7px;
        border-bottom:1px solid #eee;
        position:relative;
        border-left:3px solid #ddd;
    }
    .multiselect ul li:last-child {
        border-bottom:0
    }
    .multiselect ul li input {
        position:absolute;
        top:3px;
        left:3px;
    }
    .multiselect ul li label {
        display:block;
        width:100%;
        height:100%;
        padding-left:30px;
        -webkit-box-sizing:border-box;
        -moz-box-sizing:border-box;
        box-sizing:border-box;
        white-space:nowrap;
        text-overflow:ellipsis;
        overflow:hidden;
        font-size:12px;
        color:#333;
    }
    .multiselect ul .group-name {
        color:#444;
        border-left:0;
        margin:0;
    }
    .multiselect ul li.select {
        background:#f5f5f5;
        border-left:3px solid #89CE7E;
    }
    .multiselect ul li.select:first-child {
        box-shadow:inset 0 2px 4px -2px rgba(0, 0, 0, .2);
    }
    .multiselect ul li.select:last-child {
        box-shadow:inset 0 -2px 4px -2px rgba(0, 0, 0, .2);
    }

    .multiselect > input {
        background: #fff url(select-arrow.png) 97% center no-repeat;
        cursor:pointer;            
        padding-right:30px;
        white-space: nowrap;
        text-overflow:ellipsis;
        width:200px;
    }

/* wygląd tego elementu powinien być nadany przez layout reszty input:text w formularzach na stronie */
    .multiselect > input {
        border:1px solid #ddd;
        height:30px;
        padding-left:10px;
        width:200px;
    }
    .ie .multiselect > input {
        line-height:30px;
    }

Na koniec zostaje odpalić nasze dzieło:

<script>
$(function() {
    $('select[multiple]').multiSelectToCheckboxes({
        filterLabel : 'Szukaj:',
        filterPlaceholder : '...',
        initPlaceholder : 'Wybierz',
        minFilterTextLength : 0,
        selectAllText : 'Zaznacz widoczne',
        deselectAllText : 'Odznacz widoczne',
        maxListToCount : 4, //po przekroczeniu tej wartosci pokaze ponizszy komunikat
        infoText : 'Wybrano {0} z {1}',
        showOnlyCountText : false, //czy pokazywac tylko powyzszy tekst w polu
        closeText : 'X'
    });
})
</script>

Zobacz demo

Dodatek 1: Maksymalna liczba wybranych

Użytkownik piotrekxyz zadał ciekawe pytanie. Jak zmienić nasz multiselekt tak, aby miał możliwe ograniczenie co do maksymalnej liczby wybranych opcji.
Wbrew pozorom nie jest to jakieś olbrzymie wyzwanie.

Takie ograniczenie powinno być przede wszystkim opcjonalne, dlatego dodajemy do pluginu opcję maxSelected.
Jeżeli nie podamy tego parametru, wówczas powinniśmy przyjąć, że maksymalna liczba równa jest liczbie elementów selekta który zmieniamy.
Robimy więc wyliczenie i podstawiamy wynik pod zmienną maxSelected. Poniżej zamieszczam fragmenty kodu, który zmieniamy:

$.fn.multiSelectToCheckboxes = function(options) {
    options = $.extend({
        filterLabel : 'Szukaj:',
        filterPlaceholder : 'Szukaj',
        initPlaceholder : 'Wybierz',
        minFilterTextLength : 0,
        selectAllText : 'Zaznacz widoczne',
        deselectAllText : 'Odznacz widoczne',
        maxListToCount : 4, //po przekroczeniu tej wartosci pokaze ponizszy komunikat
        infoText : 'Wybrano {0} z {1}',
        showOnlyCountText : false, //czy pokazywac tylko powyzszy tekst w polu
        closeText : 'Zamknij',
        maxSelected : null
    }, options);
    ...
    ...

    return this.each(function() {                                
        var $select = $(this);
        var $wrapper = $('<div class="multiselect"></div>');
        $select.wrap($wrapper);
        maxSelected = (options.maxSelected!=null)?options.maxSelected : $select.children().length;
        ...
        ...
    })
};    

Gdy parametr maxSelect okreslamy na podstawie liczby elemntów selekta, musimy ją aktualizować za każdym razem gdy te elemnty dodajemy lub usuwamy. Zrobimy to w funkcji update, którą już wcześniej napisaliśmy:

...
...
function updateList($select, $ul) {
    maxSelected = (options.maxSelected!=null)?options.maxSelected : $select.children().length;
    $ul.empty();
    fillUl($select, $ul);                
};
...
...

Mamy więc ustawiony parametr który określa maksymalną liczbę checkboxów, które możemy wybrać.

Kolejnym krokiem jest sprawdzenie, czy właśnie kliknięty checkbox mieści się w tym zakresie.
W naszym pluginie obsługujemy dla checboxów zdarzenie change (patrz fillUl), które odbywa się po zmianie stanu checkboxa, czyli kod z poniższej funkcji obsługuje checkbox, który właśnie zmienił swój stan. Musimy mieć to na uwadze i w razie gdy liczba wybranych checkboxów jest większa niż dopuszczalna, wyłączać checked aktualnie zmienionego checkboxa:

function fillUl($select, $ul) {                
    ...
    ...
    var $checkbox = $("<input type='checkbox' id='" + id + "'/>")
        .appendTo($li)
        .on('change', function() {
            var $chk = $(this)                                                                
            if ($chk.is(":checked")) {
                if ($select.parent().find(':checkbox:checked').length > maxSelected) {
                    $t.removeAttr("selected");
                    $chk.removeAttr('checked');
                } else {
                    $chk.parent().addClass('select');
                    $t.attr("selected", "selected");                                    
                }
            } else {
                $chk.parent().removeClass('select');
                $t.removeAttr("selected");
            }
            updateFakeInput($select);
        });
    ...
    ...        
};

Ostatnim zadaniem jest obsługa przycisku „selectAll”.
Co właściwie powinno się dziać po kliknięciu takiego przycisku? Czy w przypadku gdy na liście widnieje więcej checkboxów powinien pojawić się stosowny komunikat? Czy powinna się wybrać tylko dopuszczalna liczba checkboxów? A może po prostu ukryć te przyciski?
My wybierzemy drugie rozwiązanie.

Dokonując zaznaczenia „wszystkich” checkbów musimy mieć na uwadze, że mogą być już zaznaczone jakieś checkboxy. Dlatego musimy wykonać odpowiednie wyliczenie.

function createMultiselectControl($select, $ul) {
    ...
    ...            
    $buttonSelectAll.on('click', function(e) {
        e.preventDefault();
        var selectedCheckboxCount = $ul.find('li:visible :checkbox:checked').length;                    

        $ul.find('li:visible :checkbox:not(:checked):lt('+(maxSelected - selectedCheckboxCount)+')').prop('checked', 1);
        $ul.find(':checkbox').trigger('change');
    })
    ...
    ...
}

I właściwie to wszystko. Gotowy efekt możesz znaleźć poniżej:

Zobacz demo

Dodatek 2: Wyłączenie wielkości liter w polu filtra

Kolejną małą poprawką będzie wyłączenie uwzględnianai wielkości liter w polu filtra. Aby to zrobić skorzystamy
z przepisu na stronie http://stackoverflow.com/questions/187537/is-there-a-case-insensitive-jquery-contains-selector

Od naszego kodu dodajemy więc kod z tamtej odpowiedzi i wykorzystujemy go w :

(function($) {
    $.extend($.expr[':'], {
        'containsi': function(elem, i, match, array) {
            return (elem.textContent || elem.innerText || '').toLowerCase().indexOf((match[3] || "").toLowerCase()) >= 0;
        }
    });
 
    $.fn.multiSelectToCheckboxes = function(options) {
        ...
        function createMultiselectControl($select, $ul) 
            ...
            $filterInput.on('keyup', function() {
                ...
                if (val.length >= options.minFilterTextLength) {                        
                    $ul.children('li').hide();
                    $ul.children('li').filter(':containsi("'+val+'")').show();
                }  
                ...
            }
            ...
        }
        ...
    }
})();

Komentarze

  • piortrekzxc

    swietny tutorial!

    z reszta cala strona jest pelna przydatnych rozwiazan.
    mam
    jedno pytanie dotyczace multiselecta, co nalezy zrobic zeby po wyborze
    np. 3 opcji wylaczyc mozliwosc wyboru kolenjych, czyli sprawic zeby
    pozostale opcje nie mogy juz zostac zaznaczone.

    • kartofelek007

      Dodam to do tutka jako mini rozwinięcie.

  • Guest

    Wielkie dzieki! dokladnie o to chodzilo

  • Kowal

    Na wstępnie pragnę podziękować za kawał dobrej roboty.
    Mam pytanie jak mogę osiągalność coś takiego, że po wywołaniu pewnej akcji wszystkie checkboxsy w multiselect zostaną odznaczone a okno zamknięte?

    • kartofelek007

      Kod właściwie masz już powyżej :)
      1) Pobierasz dany multiselekt, zaznaczasz/odznaczasz wszystko (tak jak w guziku $buttonSelectAll)
      2) ukrywasz kontener z kontrolkami multiselekta
      Mniej więcej coś takiego:

      var $multiselekt = $(‚select[name=….]’).parent(); //pobieramy selekt po name – dokladnie jego wrapper
      var $ul = $multiselekt.find(‚ul’); //to zupelnie tak samo jak w guziku $buttonSelectAll
      $ul.find(‚li:visible :checkbox’).prop(‚checked’, 1);
      $ul.find(‚:checkbox’).trigger(‚change’);
      $multiselekt.find(‚.multiselect-cnt’).hide(); //ukrywamy container

      Pamiętaj, że w powyższym kodzie nie masz wykrywania maksymalnej liczy zaznaczeń. Jeżeli w twojej wersji ma się to pojawić, po prostu dodaj to tak jak wyjaśniłem na końcu tutka :)

  • AlikStudio

    Oooops! Znalazłem błąd :D
    Kiedy do pola szukaj wpisuję jakieś głupoty, których nie ma na liście (np. ddddddddddddddddd) za każdym wpisaniem kolejnej literki lista się powiększa. (Używam Chromu)

    • kartofelek007

      Prawda to.

      Linijkę

      $ul.parent().append(‚Brak wyników‚)

      zamień na:

      if (!$ul.parent().find(‚.empty-list’).length) $ul.parent().append(‚Brak wyników‚)