Administrator
2022-09-14 58d006e05dcf2a20d0ec5367dd03d66a61db6849
提交 | 用户 | 时间
58d006 1 /**
A 2  * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect)
3  * 
4  * Apache License, Version 2.0:
5  * Copyright (c) 2012 - 2015 David Stutz
6  * 
7  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
8  * use this file except in compliance with the License. You may obtain a
9  * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10  * 
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14  * License for the specific language governing permissions and limitations
15  * under the License.
16  * 
17  * BSD 3-Clause License:
18  * Copyright (c) 2012 - 2015 David Stutz
19  * All rights reserved.
20  * 
21  * Redistribution and use in source and binary forms, with or without
22  * modification, are permitted provided that the following conditions are met:
23  *    - Redistributions of source code must retain the above copyright notice,
24  *      this list of conditions and the following disclaimer.
25  *    - Redistributions in binary form must reproduce the above copyright notice,
26  *      this list of conditions and the following disclaimer in the documentation
27  *      and/or other materials provided with the distribution.
28  *    - Neither the name of David Stutz nor the names of its contributors may be
29  *      used to endorse or promote products derived from this software without
30  *      specific prior written permission.
31  * 
32  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
33  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
34  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
35  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
36  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
37  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
39  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
40  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
41  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43  */
44 !function ($) {
45     "use strict";// jshint ;_;
46
47     if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
48         ko.bindingHandlers.multiselect = {
49             after: ['options', 'value', 'selectedOptions'],
50
51             init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
52                 var $element = $(element);
53                 var config = ko.toJS(valueAccessor());
54
55                 $element.multiselect(config);
56
57                 if (allBindings.has('options')) {
58                     var options = allBindings.get('options');
59                     if (ko.isObservable(options)) {
60                         ko.computed({
61                             read: function() {
62                                 options();
63                                 setTimeout(function() {
64                                     var ms = $element.data('multiselect');
65                                     if (ms)
66                                         ms.updateOriginalOptions();//Not sure how beneficial this is.
67                                     $element.multiselect('rebuild');
68                                 }, 1);
69                             },
70                             disposeWhenNodeIsRemoved: element
71                         });
72                     }
73                 }
74
75                 //value and selectedOptions are two-way, so these will be triggered even by our own actions.
76                 //It needs some way to tell if they are triggered because of us or because of outside change.
77                 //It doesn't loop but it's a waste of processing.
78                 if (allBindings.has('value')) {
79                     var value = allBindings.get('value');
80                     if (ko.isObservable(value)) {
81                         ko.computed({
82                             read: function() {
83                                 value();
84                                 setTimeout(function() {
85                                     $element.multiselect('refresh');
86                                 }, 1);
87                             },
88                             disposeWhenNodeIsRemoved: element
89                         }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
90                     }
91                 }
92
93                 //Switched from arrayChange subscription to general subscription using 'refresh'.
94                 //Not sure performance is any better using 'select' and 'deselect'.
95                 if (allBindings.has('selectedOptions')) {
96                     var selectedOptions = allBindings.get('selectedOptions');
97                     if (ko.isObservable(selectedOptions)) {
98                         ko.computed({
99                             read: function() {
100                                 selectedOptions();
101                                 setTimeout(function() {
102                                     $element.multiselect('refresh');
103                                 }, 1);
104                             },
105                             disposeWhenNodeIsRemoved: element
106                         }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
107                     }
108                 }
109
110                 ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
111                     $element.multiselect('destroy');
112                 });
113             },
114
115             update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
116                 var $element = $(element);
117                 var config = ko.toJS(valueAccessor());
118
119                 $element.multiselect('setOptions', config);
120                 $element.multiselect('rebuild');
121             }
122         };
123     }
124
125     function forEach(array, callback) {
126         for (var index = 0; index < array.length; ++index) {
127             callback(array[index], index);
128         }
129     }
130
131     /**
132      * Constructor to create a new multiselect using the given select.
133      *
134      * @param {jQuery} select
135      * @param {Object} options
136      * @returns {Multiselect}
137      */
138     function Multiselect(select, options) {
139
140         this.$select = $(select);
141         
142         // Placeholder via data attributes
143         if (this.$select.attr("data-placeholder")) {
144             options.nonSelectedText = this.$select.data("placeholder");
145         }
146         
147         this.options = this.mergeOptions($.extend({}, options, this.$select.data()));
148
149         // Initialization.
150         // We have to clone to create a new reference.
151         this.originalOptions = this.$select.clone()[0].options;
152         this.query = '';
153         this.searchTimeout = null;
154         this.lastToggledInput = null
155
156         this.options.multiple = this.$select.attr('multiple') === "multiple";
157         this.options.onChange = $.proxy(this.options.onChange, this);
158         this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
159         this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
160         this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this);
161         this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this);
162         
163         // Build select all if enabled.
164         this.buildContainer();
165         this.buildButton();
166         this.buildDropdown();
167         this.buildSelectAll();
168         this.buildDropdownOptions();
169         this.buildFilter();
170
171         this.updateButtonText();
172         this.updateSelectAll();
173
174         if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
175             this.disable();
176         }
177         
178         this.$select.hide().after(this.$container);
179     };
180
181     Multiselect.prototype = {
182
183         defaults: {
184             /**
185              * Default text function will either print 'None selected' in case no
186              * option is selected or a list of the selected options up to a length
187              * of 3 selected options.
188              * 
189              * @param {jQuery} options
190              * @param {jQuery} select
191              * @returns {String}
192              */
193             buttonText: function(options, select) {
194                 if (options.length === 0) {
195                     return this.nonSelectedText;
196                 }
197                 else if (this.allSelectedText 
198                             && options.length === $('option', $(select)).length 
199                             && $('option', $(select)).length !== 1 
200                             && this.multiple) {
201
202                     if (this.selectAllNumber) {
203                         return this.allSelectedText + ' (' + options.length + ')';
204                     }
205                     else {
206                         return this.allSelectedText;
207                     }
208                 }
209                 else if (options.length > this.numberDisplayed) {
210                     return options.length + ' ' + this.nSelectedText;
211                 }
212                 else {
213                     var selected = '';
214                     var delimiter = this.delimiterText;
215                     
216                     options.each(function() {
217                         var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
218                         selected += label + delimiter;
219                     });
220                     
221                     return selected.substr(0, selected.length - 2);
222                 }
223             },
224             /**
225              * Updates the title of the button similar to the buttonText function.
226              * 
227              * @param {jQuery} options
228              * @param {jQuery} select
229              * @returns {@exp;selected@call;substr}
230              */
231             buttonTitle: function(options, select) {
232                 if (options.length === 0) {
233                     return this.nonSelectedText;
234                 }
235                 else {
236                     var selected = '';
237                     var delimiter = this.delimiterText;
238                     
239                     options.each(function () {
240                         var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
241                         selected += label + delimiter;
242                     });
243                     return selected.substr(0, selected.length - 2);
244                 }
245             },
246             /**
247              * Create a label.
248              *
249              * @param {jQuery} element
250              * @returns {String}
251              */
252             optionLabel: function(element){
253                 return $(element).attr('label') || $(element).text();
254             },
255             /**
256              * Triggered on change of the multiselect.
257              * 
258              * Not triggered when selecting/deselecting options manually.
259              * 
260              * @param {jQuery} option
261              * @param {Boolean} checked
262              */
263             onChange : function(option, checked) {
264
265             },
266             /**
267              * Triggered when the dropdown is shown.
268              *
269              * @param {jQuery} event
270              */
271             onDropdownShow: function(event) {
272
273             },
274             /**
275              * Triggered when the dropdown is hidden.
276              *
277              * @param {jQuery} event
278              */
279             onDropdownHide: function(event) {
280
281             },
282             /**
283              * Triggered after the dropdown is shown.
284              * 
285              * @param {jQuery} event
286              */
287             onDropdownShown: function(event) {
288                 
289             },
290             /**
291              * Triggered after the dropdown is hidden.
292              * 
293              * @param {jQuery} event
294              */
295             onDropdownHidden: function(event) {
296                 
297             },
298             /**
299              * Triggered on select all.
300              */
301             onSelectAll: function() {
302                 
303             },
304             enableHTML: false,
305             buttonClass: 'btn btn-default',
306             inheritClass: false,
307             buttonWidth: 'auto',
308             buttonContainer: '<div class="btn-group" />',
309             dropRight: false,
310             selectedClass: 'active',
311             // Maximum height of the dropdown menu.
312             // If maximum height is exceeded a scrollbar will be displayed.
313             maxHeight: false,
314             checkboxName: false,
315             includeSelectAllOption: false,
316             includeSelectAllIfMoreThan: 0,
317             selectAllText: ' Select all',
318             selectAllValue: 'multiselect-all',
319             selectAllName: false,
320             selectAllNumber: true,
321             enableFiltering: false,
322             enableCaseInsensitiveFiltering: false,
323             enableClickableOptGroups: false,
324             filterPlaceholder: 'Search',
325             // possible options: 'text', 'value', 'both'
326             filterBehavior: 'text',
327             includeFilterClearBtn: true,
328             preventInputChangeEvent: false,
329             nonSelectedText: 'None selected',
330             nSelectedText: 'selected',
331             allSelectedText: 'All selected',
332             numberDisplayed: 3,
333             disableIfEmpty: false,
334             delimiterText: ', ',
335             templates: {
336                 button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span> <b class="caret"></b></button>',
337                 ul: '<ul class="multiselect-container dropdown-menu"></ul>',
338                 filter: '<li class="multiselect-item filter"><div class="input-group"><span class="input-group-addon"><i class="glyphicon glyphicon-search"></i></span><input class="form-control multiselect-search" type="text"></div></li>',
339                 filterClearBtn: '<span class="input-group-btn"><button class="btn btn-default multiselect-clear-filter" type="button"><i class="glyphicon glyphicon-remove-circle"></i></button></span>',
340                 li: '<li><a tabindex="0"><label></label></a></li>',
341                 divider: '<li class="multiselect-item divider"></li>',
342                 liGroup: '<li class="multiselect-item multiselect-group"><label></label></li>'
343             }
344         },
345
346         constructor: Multiselect,
347
348         /**
349          * Builds the container of the multiselect.
350          */
351         buildContainer: function() {
352             this.$container = $(this.options.buttonContainer);
353             this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
354             this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
355             this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
356             this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
357         },
358
359         /**
360          * Builds the button of the multiselect.
361          */
362         buildButton: function() {
363             this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
364             if (this.$select.attr('class') && this.options.inheritClass) {
365                 this.$button.addClass(this.$select.attr('class'));
366             }
367             // Adopt active state.
368             if (this.$select.prop('disabled')) {
369                 this.disable();
370             }
371             else {
372                 this.enable();
373             }
374
375             // Manually add button width if set.
376             if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
377                 this.$button.css({
378                     'width' : this.options.buttonWidth,
379                     'overflow' : 'hidden',
380                     'text-overflow' : 'ellipsis'
381                 });
382                 this.$container.css({
383                     'width': this.options.buttonWidth
384                 });
385             }
386
387             // Keep the tab index from the select.
388             var tabindex = this.$select.attr('tabindex');
389             if (tabindex) {
390                 this.$button.attr('tabindex', tabindex);
391             }
392
393             this.$container.prepend(this.$button);
394         },
395
396         /**
397          * Builds the ul representing the dropdown menu.
398          */
399         buildDropdown: function() {
400
401             // Build ul.
402             this.$ul = $(this.options.templates.ul);
403
404             if (this.options.dropRight) {
405                 this.$ul.addClass('pull-right');
406             }
407
408             // Set max height of dropdown menu to activate auto scrollbar.
409             if (this.options.maxHeight) {
410                 // TODO: Add a class for this option to move the css declarations.
411                 this.$ul.css({
412                     'max-height': this.options.maxHeight + 'px',
413                     'overflow-y': 'auto',
414                     'overflow-x': 'hidden'
415                 });
416             }
417
418             this.$container.append(this.$ul);
419         },
420
421         /**
422          * Build the dropdown options and binds all nessecary events.
423          * 
424          * Uses createDivider and createOptionValue to create the necessary options.
425          */
426         buildDropdownOptions: function() {
427
428             this.$select.children().each($.proxy(function(index, element) {
429
430                 var $element = $(element);
431                 // Support optgroups and options without a group simultaneously.
432                 var tag = $element.prop('tagName')
433                     .toLowerCase();
434             
435                 if ($element.prop('value') === this.options.selectAllValue) {
436                     return;
437                 }
438
439                 if (tag === 'optgroup') {
440                     this.createOptgroup(element);
441                 }
442                 else if (tag === 'option') {
443
444                     if ($element.data('role') === 'divider') {
445                         this.createDivider();
446                     }
447                     else {
448                         this.createOptionValue(element);
449                     }
450
451                 }
452
453                 // Other illegal tags will be ignored.
454             }, this));
455
456             // Bind the change event on the dropdown elements.
457             $('li input', this.$ul).on('change', $.proxy(function(event) {
458                 var $target = $(event.target);
459
460                 var checked = $target.prop('checked') || false;
461                 var isSelectAllOption = $target.val() === this.options.selectAllValue;
462
463                 // Apply or unapply the configured selected class.
464                 if (this.options.selectedClass) {
465                     if (checked) {
466                         $target.closest('li')
467                             .addClass(this.options.selectedClass);
468                     }
469                     else {
470                         $target.closest('li')
471                             .removeClass(this.options.selectedClass);
472                     }
473                 }
474
475                 // Get the corresponding option.
476                 var value = $target.val();
477                 var $option = this.getOptionByValue(value);
478
479                 var $optionsNotThis = $('option', this.$select).not($option);
480                 var $checkboxesNotThis = $('input', this.$container).not($target);
481
482                 if (isSelectAllOption) {
483                     if (checked) {
484                         this.selectAll();
485                     }
486                     else {
487                         this.deselectAll();
488                     }
489                 }
490
491                 if(!isSelectAllOption){
492                     if (checked) {
493                         $option.prop('selected', true);
494
495                         if (this.options.multiple) {
496                             // Simply select additional option.
497                             $option.prop('selected', true);
498                         }
499                         else {
500                             // Unselect all other options and corresponding checkboxes.
501                             if (this.options.selectedClass) {
502                                 $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass);
503                             }
504
505                             $($checkboxesNotThis).prop('checked', false);
506                             $optionsNotThis.prop('selected', false);
507
508                             // It's a single selection, so close.
509                             this.$button.click();
510                         }
511
512                         if (this.options.selectedClass === "active") {
513                             $optionsNotThis.closest("a").css("outline", "");
514                         }
515                     }
516                     else {
517                         // Unselect option.
518                         $option.prop('selected', false);
519                     }
520                 }
521
522                 this.$select.change();
523
524                 this.updateButtonText();
525                 this.updateSelectAll();
526
527                 this.options.onChange($option, checked);
528
529                 if(this.options.preventInputChangeEvent) {
530                     return false;
531                 }
532             }, this));
533
534             $('li a', this.$ul).on('mousedown', function(e) {
535                 if (e.shiftKey) {
536                     // Prevent selecting text by Shift+click
537                     return false;
538                 }
539             });
540         
541             $('li a', this.$ul).on('touchstart click', $.proxy(function(event) {
542                 event.stopPropagation();
543
544                 var $target = $(event.target);
545                 
546                 if (event.shiftKey && this.options.multiple) {
547                     if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
548                         event.preventDefault();
549                         $target = $target.find("input");
550                         $target.prop("checked", !$target.prop("checked"));
551                     }
552                     var checked = $target.prop('checked') || false;
553
554                     if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
555                         var from = $target.closest("li").index();
556                         var to = this.lastToggledInput.closest("li").index();
557                         
558                         if (from > to) { // Swap the indices
559                             var tmp = to;
560                             to = from;
561                             from = tmp;
562                         }
563                         
564                         // Make sure we grab all elements since slice excludes the last index
565                         ++to;
566                         
567                         // Change the checkboxes and underlying options
568                         var range = this.$ul.find("li").slice(from, to).find("input");
569                         
570                         range.prop('checked', checked);
571                         
572                         if (this.options.selectedClass) {
573                             range.closest('li')
574                                 .toggleClass(this.options.selectedClass, checked);
575                         }
576                         
577                         for (var i = 0, j = range.length; i < j; i++) {
578                             var $checkbox = $(range[i]);
579
580                             var $option = this.getOptionByValue($checkbox.val());
581
582                             $option.prop('selected', checked);
583                         }                   
584                     }
585                     
586                     // Trigger the select "change" event
587                     $target.trigger("change");
588                 }
589                 
590                 // Remembers last clicked option
591                 if($target.is("input") && !$target.closest("li").is(".multiselect-item")){
592                     this.lastToggledInput = $target;
593                 }
594
595                 $target.blur();
596             }, this));
597
598             // Keyboard support.
599             this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) {
600                 if ($('input[type="text"]', this.$container).is(':focus')) {
601                     return;
602                 }
603
604                 if (event.keyCode === 9 && this.$container.hasClass('open')) {
605                     this.$button.click();
606                 }
607                 else {
608                     var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible");
609
610                     if (!$items.length) {
611                         return;
612                     }
613
614                     var index = $items.index($items.filter(':focus'));
615
616                     // Navigation up.
617                     if (event.keyCode === 38 && index > 0) {
618                         index--;
619                     }
620                     // Navigate down.
621                     else if (event.keyCode === 40 && index < $items.length - 1) {
622                         index++;
623                     }
624                     else if (!~index) {
625                         index = 0;
626                     }
627
628                     var $current = $items.eq(index);
629                     $current.focus();
630
631                     if (event.keyCode === 32 || event.keyCode === 13) {
632                         var $checkbox = $current.find('input');
633
634                         $checkbox.prop("checked", !$checkbox.prop("checked"));
635                         $checkbox.change();
636                     }
637
638                     event.stopPropagation();
639                     event.preventDefault();
640                 }
641             }, this));
642
643             if(this.options.enableClickableOptGroups && this.options.multiple) {
644                 $('li.multiselect-group', this.$ul).on('click', $.proxy(function(event) {
645                     event.stopPropagation();
646
647                     var group = $(event.target).parent();
648
649                     // Search all option in optgroup
650                     var $options = group.nextUntil('li.multiselect-group');
651                     var $visibleOptions = $options.filter(":visible:not(.disabled)");
652
653                     // check or uncheck items
654                     var allChecked = true;
655                     var optionInputs = $visibleOptions.find('input');
656                     optionInputs.each(function() {
657                         allChecked = allChecked && $(this).prop('checked');
658                     });
659
660                     optionInputs.prop('checked', !allChecked).trigger('change');
661                }, this));
662             }
663         },
664
665         /**
666          * Create an option using the given select option.
667          *
668          * @param {jQuery} element
669          */
670         createOptionValue: function(element) {
671             var $element = $(element);
672             if ($element.is(':selected')) {
673                 $element.prop('selected', true);
674             }
675
676             // Support the label attribute on options.
677             var label = this.options.optionLabel(element);
678             var value = $element.val();
679             var inputType = this.options.multiple ? "checkbox" : "radio";
680
681             var $li = $(this.options.templates.li);
682             var $label = $('label', $li);
683             $label.addClass(inputType);
684
685             if (this.options.enableHTML) {
686                 $label.html(" " + label);
687             }
688             else {
689                 $label.text(" " + label);
690             }
691         
692             var $checkbox = $('<input/>').attr('type', inputType).addClass('ace');//ACE
693
694             if (this.options.checkboxName) {
695                 $checkbox.attr('name', this.options.checkboxName);
696             }
697             $label.prepend($checkbox);
698             $checkbox.after('<span class="lbl" />');//ACE
699
700             var selected = $element.prop('selected') || false;
701             $checkbox.val(value);
702
703             if (value === this.options.selectAllValue) {
704                 $li.addClass("multiselect-item multiselect-all");
705                 $checkbox.parent().parent()
706                     .addClass('multiselect-all');
707             }
708
709             $label.attr('title', $element.attr('title'));
710
711             this.$ul.append($li);
712
713             if ($element.is(':disabled')) {
714                 $checkbox.attr('disabled', 'disabled')
715                     .prop('disabled', true)
716                     .closest('a')
717                     .attr("tabindex", "-1")
718                     .closest('li')
719                     .addClass('disabled');
720             }
721
722             $checkbox.prop('checked', selected);
723
724             if (selected && this.options.selectedClass) {
725                 $checkbox.closest('li')
726                     .addClass(this.options.selectedClass);
727             }
728         },
729
730         /**
731          * Creates a divider using the given select option.
732          *
733          * @param {jQuery} element
734          */
735         createDivider: function(element) {
736             var $divider = $(this.options.templates.divider);
737             this.$ul.append($divider);
738         },
739
740         /**
741          * Creates an optgroup.
742          *
743          * @param {jQuery} group
744          */
745         createOptgroup: function(group) {
746             var groupName = $(group).prop('label');
747
748             // Add a header for the group.
749             var $li = $(this.options.templates.liGroup);
750             
751             if (this.options.enableHTML) {
752                 $('label', $li).html(groupName);
753             }
754             else {
755                 $('label', $li).text(groupName);
756             }
757             
758             if (this.options.enableClickableOptGroups) {
759                 $li.addClass('multiselect-group-clickable');
760             }
761
762             this.$ul.append($li);
763
764             if ($(group).is(':disabled')) {
765                 $li.addClass('disabled');
766             }
767
768             // Add the options of the group.
769             $('option', group).each($.proxy(function(index, element) {
770                 this.createOptionValue(element);
771             }, this));
772         },
773
774         /**
775          * Build the selct all.
776          * 
777          * Checks if a select all has already been created.
778          */
779         buildSelectAll: function() {
780             if (typeof this.options.selectAllValue === 'number') {
781                 this.options.selectAllValue = this.options.selectAllValue.toString();
782             }
783             
784             var alreadyHasSelectAll = this.hasSelectAll();
785
786             if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
787                     && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) {
788
789                 // Check whether to add a divider after the select all.
790                 if (this.options.includeSelectAllDivider) {
791                     this.$ul.prepend($(this.options.templates.divider));
792                 }
793
794                 var $li = $(this.options.templates.li);
795                 $('label', $li).addClass("checkbox");
796                 
797                 if (this.options.enableHTML) {
798                     $('label', $li).html(" " + this.options.selectAllText);
799                 }
800                 else {
801                     $('label', $li).text(" " + this.options.selectAllText);
802                 }
803                 
804                 if (this.options.selectAllName) {
805                     $('label', $li).prepend('<input type="checkbox" name="' + this.options.selectAllName + '" />');
806                 }
807                 else {
808                     $('label', $li).prepend('<input type="checkbox" />');
809                 }
810                 
811                 var $checkbox = $('input', $li);
812                 $checkbox.val(this.options.selectAllValue);
813
814                 $li.addClass("multiselect-item multiselect-all");
815                 $checkbox.parent().parent()
816                     .addClass('multiselect-all');
817
818                 this.$ul.prepend($li);
819
820                 $checkbox.prop('checked', false);
821             }
822         },
823
824         /**
825          * Builds the filter.
826          */
827         buildFilter: function() {
828
829             // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
830             if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
831                 var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
832
833                 if (this.$select.find('option').length >= enableFilterLength) {
834
835                     this.$filter = $(this.options.templates.filter);
836                     $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
837                     
838                     // Adds optional filter clear button
839                     if(this.options.includeFilterClearBtn){
840                         var clearBtn = $(this.options.templates.filterClearBtn);
841                         clearBtn.on('click', $.proxy(function(event){
842                             clearTimeout(this.searchTimeout);
843                             this.$filter.find('.multiselect-search').val('');
844                             $('li', this.$ul).show().removeClass("filter-hidden");
845                             this.updateSelectAll();
846                         }, this));
847                         this.$filter.find('.input-group').append(clearBtn);
848                     }
849                     
850                     this.$ul.prepend(this.$filter);
851
852                     this.$filter.val(this.query).on('click', function(event) {
853                         event.stopPropagation();
854                     }).on('input keydown', $.proxy(function(event) {
855                         // Cancel enter key default behaviour
856                         if (event.which === 13) {
857                           event.preventDefault();
858                         }
859                         
860                         // This is useful to catch "keydown" events after the browser has updated the control.
861                         clearTimeout(this.searchTimeout);
862
863                         this.searchTimeout = this.asyncFunction($.proxy(function() {
864
865                             if (this.query !== event.target.value) {
866                                 this.query = event.target.value;
867
868                                 var currentGroup, currentGroupVisible;
869                                 $.each($('li', this.$ul), $.proxy(function(index, element) {
870                                     var value = $('input', element).length > 0 ? $('input', element).val() : "";
871                                     var text = $('label', element).text();
872
873                                     var filterCandidate = '';
874                                     if ((this.options.filterBehavior === 'text')) {
875                                         filterCandidate = text;
876                                     }
877                                     else if ((this.options.filterBehavior === 'value')) {
878                                         filterCandidate = value;
879                                     }
880                                     else if (this.options.filterBehavior === 'both') {
881                                         filterCandidate = text + '\n' + value;
882                                     }
883
884                                     if (value !== this.options.selectAllValue && text) {
885                                         // By default lets assume that element is not
886                                         // interesting for this search.
887                                         var showElement = false;
888
889                                         if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
890                                             showElement = true;
891                                         }
892                                         else if (filterCandidate.indexOf(this.query) > -1) {
893                                             showElement = true;
894                                         }
895
896                                         // Toggle current element (group or group item) according to showElement boolean.
897                                         $(element).toggle(showElement).toggleClass('filter-hidden', !showElement);
898                                         
899                                         // Differentiate groups and group items.
900                                         if ($(element).hasClass('multiselect-group')) {
901                                             // Remember group status.
902                                             currentGroup = element;
903                                             currentGroupVisible = showElement;
904                                         }
905                                         else {
906                                             // Show group name when at least one of its items is visible.
907                                             if (showElement) {
908                                                 $(currentGroup).show().removeClass('filter-hidden');
909                                             }
910                                             
911                                             // Show all group items when group name satisfies filter.
912                                             if (!showElement && currentGroupVisible) {
913                                                 $(element).show().removeClass('filter-hidden');
914                                             }
915                                         }
916                                     }
917                                 }, this));
918                             }
919
920                             this.updateSelectAll();
921                         }, this), 300, this);
922                     }, this));
923                 }
924             }
925         },
926
927         /**
928          * Unbinds the whole plugin.
929          */
930         destroy: function() {
931             this.$container.remove();
932             this.$select.show();
933             this.$select.data('multiselect', null);
934         },
935
936         /**
937          * Refreshs the multiselect based on the selected options of the select.
938          */
939         refresh: function() {
940             $('option', this.$select).each($.proxy(function(index, element) {
941                 var $input = $('li input', this.$ul).filter(function() {
942                     return $(this).val() === $(element).val();
943                 });
944
945                 if ($(element).is(':selected')) {
946                     $input.prop('checked', true);
947
948                     if (this.options.selectedClass) {
949                         $input.closest('li')
950                             .addClass(this.options.selectedClass);
951                     }
952                 }
953                 else {
954                     $input.prop('checked', false);
955
956                     if (this.options.selectedClass) {
957                         $input.closest('li')
958                             .removeClass(this.options.selectedClass);
959                     }
960                 }
961
962                 if ($(element).is(":disabled")) {
963                     $input.attr('disabled', 'disabled')
964                         .prop('disabled', true)
965                         .closest('li')
966                         .addClass('disabled');
967                 }
968                 else {
969                     $input.prop('disabled', false)
970                         .closest('li')
971                         .removeClass('disabled');
972                 }
973             }, this));
974
975             this.updateButtonText();
976             this.updateSelectAll();
977         },
978
979         /**
980          * Select all options of the given values.
981          * 
982          * If triggerOnChange is set to true, the on change event is triggered if
983          * and only if one value is passed.
984          * 
985          * @param {Array} selectValues
986          * @param {Boolean} triggerOnChange
987          */
988         select: function(selectValues, triggerOnChange) {
989             if(!$.isArray(selectValues)) {
990                 selectValues = [selectValues];
991             }
992
993             for (var i = 0; i < selectValues.length; i++) {
994                 var value = selectValues[i];
995
996                 if (value === null || value === undefined) {
997                     continue;
998                 }
999
1000                 var $option = this.getOptionByValue(value);
1001                 var $checkbox = this.getInputByValue(value);
1002
1003                 if($option === undefined || $checkbox === undefined) {
1004                     continue;
1005                 }
1006                 
1007                 if (!this.options.multiple) {
1008                     this.deselectAll(false);
1009                 }
1010                 
1011                 if (this.options.selectedClass) {
1012                     $checkbox.closest('li')
1013                         .addClass(this.options.selectedClass);
1014                 }
1015
1016                 $checkbox.prop('checked', true);
1017                 $option.prop('selected', true);
1018                 
1019                 if (triggerOnChange) {
1020                     this.options.onChange($option, true);
1021                 }
1022             }
1023
1024             this.updateButtonText();
1025             this.updateSelectAll();
1026         },
1027
1028         /**
1029          * Clears all selected items.
1030          */
1031         clearSelection: function () {
1032             this.deselectAll(false);
1033             this.updateButtonText();
1034             this.updateSelectAll();
1035         },
1036
1037         /**
1038          * Deselects all options of the given values.
1039          * 
1040          * If triggerOnChange is set to true, the on change event is triggered, if
1041          * and only if one value is passed.
1042          * 
1043          * @param {Array} deselectValues
1044          * @param {Boolean} triggerOnChange
1045          */
1046         deselect: function(deselectValues, triggerOnChange) {
1047             if(!$.isArray(deselectValues)) {
1048                 deselectValues = [deselectValues];
1049             }
1050
1051             for (var i = 0; i < deselectValues.length; i++) {
1052                 var value = deselectValues[i];
1053
1054                 if (value === null || value === undefined) {
1055                     continue;
1056                 }
1057
1058                 var $option = this.getOptionByValue(value);
1059                 var $checkbox = this.getInputByValue(value);
1060
1061                 if($option === undefined || $checkbox === undefined) {
1062                     continue;
1063                 }
1064
1065                 if (this.options.selectedClass) {
1066                     $checkbox.closest('li')
1067                         .removeClass(this.options.selectedClass);
1068                 }
1069
1070                 $checkbox.prop('checked', false);
1071                 $option.prop('selected', false);
1072                 
1073                 if (triggerOnChange) {
1074                     this.options.onChange($option, false);
1075                 }
1076             }
1077
1078             this.updateButtonText();
1079             this.updateSelectAll();
1080         },
1081         
1082         /**
1083          * Selects all enabled & visible options.
1084          *
1085          * If justVisible is true or not specified, only visible options are selected.
1086          *
1087          * @param {Boolean} justVisible
1088          * @param {Boolean} triggerOnSelectAll
1089          */
1090         selectAll: function (justVisible, triggerOnSelectAll) {
1091             var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1092             var allCheckboxes = $("li input[type='checkbox']:enabled", this.$ul);
1093             var visibleCheckboxes = allCheckboxes.filter(":visible");
1094             var allCheckboxesCount = allCheckboxes.length;
1095             var visibleCheckboxesCount = visibleCheckboxes.length;
1096             
1097             if(justVisible) {
1098                 visibleCheckboxes.prop('checked', true);
1099                 $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").addClass(this.options.selectedClass);
1100             }
1101             else {
1102                 allCheckboxes.prop('checked', true);
1103                 $("li:not(.divider):not(.disabled)", this.$ul).addClass(this.options.selectedClass);
1104             }
1105                 
1106             if (allCheckboxesCount === visibleCheckboxesCount || justVisible === false) {
1107                 $("option:enabled", this.$select).prop('selected', true);
1108             }
1109             else {
1110                 var values = visibleCheckboxes.map(function() {
1111                     return $(this).val();
1112                 }).get();
1113                 
1114                 $("option:enabled", this.$select).filter(function(index) {
1115                     return $.inArray($(this).val(), values) !== -1;
1116                 }).prop('selected', true);
1117             }
1118             
1119             if (triggerOnSelectAll) {
1120                 this.options.onSelectAll();
1121             }
1122         },
1123
1124         /**
1125          * Deselects all options.
1126          * 
1127          * If justVisible is true or not specified, only visible options are deselected.
1128          * 
1129          * @param {Boolean} justVisible
1130          */
1131         deselectAll: function (justVisible) {
1132             var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1133             
1134             if(justVisible) {              
1135                 var visibleCheckboxes = $("li input[type='checkbox']:not(:disabled)", this.$ul).filter(":visible");
1136                 visibleCheckboxes.prop('checked', false);
1137                 
1138                 var values = visibleCheckboxes.map(function() {
1139                     return $(this).val();
1140                 }).get();
1141                 
1142                 $("option:enabled", this.$select).filter(function(index) {
1143                     return $.inArray($(this).val(), values) !== -1;
1144                 }).prop('selected', false);
1145                 
1146                 if (this.options.selectedClass) {
1147                     $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").removeClass(this.options.selectedClass);
1148                 }
1149             }
1150             else {
1151                 $("li input[type='checkbox']:enabled", this.$ul).prop('checked', false);
1152                 $("option:enabled", this.$select).prop('selected', false);
1153                 
1154                 if (this.options.selectedClass) {
1155                     $("li:not(.divider):not(.disabled)", this.$ul).removeClass(this.options.selectedClass);
1156                 }
1157             }
1158         },
1159
1160         /**
1161          * Rebuild the plugin.
1162          * 
1163          * Rebuilds the dropdown, the filter and the select all option.
1164          */
1165         rebuild: function() {
1166             this.$ul.html('');
1167
1168             // Important to distinguish between radios and checkboxes.
1169             this.options.multiple = this.$select.attr('multiple') === "multiple";
1170
1171             this.buildSelectAll();
1172             this.buildDropdownOptions();
1173             this.buildFilter();
1174
1175             this.updateButtonText();
1176             this.updateSelectAll();
1177             
1178             if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
1179                 this.disable();
1180             }
1181             else {
1182                 this.enable();
1183             }
1184             
1185             if (this.options.dropRight) {
1186                 this.$ul.addClass('pull-right');
1187             }
1188         },
1189
1190         /**
1191          * The provided data will be used to build the dropdown.
1192          */
1193         dataprovider: function(dataprovider) {
1194             
1195             var groupCounter = 0;
1196             var $select = this.$select.empty();
1197             
1198             $.each(dataprovider, function (index, option) {
1199                 var $tag;
1200                 
1201                 if ($.isArray(option.children)) { // create optiongroup tag
1202                     groupCounter++;
1203                     
1204                     $tag = $('<optgroup/>').attr({
1205                         label: option.label || 'Group ' + groupCounter,
1206                         disabled: !!option.disabled
1207                     });
1208                     
1209                     forEach(option.children, function(subOption) { // add children option tags
1210                         $tag.append($('<option/>').attr({
1211                             value: subOption.value,
1212                             label: subOption.label || subOption.value,
1213                             title: subOption.title,
1214                             selected: !!subOption.selected,
1215                             disabled: !!subOption.disabled
1216                         }));
1217                     });
1218                 }
1219                 else {
1220                     $tag = $('<option/>').attr({
1221                         value: option.value,
1222                         label: option.label || option.value,
1223                         title: option.title,
1224                         selected: !!option.selected,
1225                         disabled: !!option.disabled
1226                     });
1227                 }
1228                 
1229                 $select.append($tag);
1230             });
1231             
1232             this.rebuild();
1233         },
1234
1235         /**
1236          * Enable the multiselect.
1237          */
1238         enable: function() {
1239             this.$select.prop('disabled', false);
1240             this.$button.prop('disabled', false)
1241                 .removeClass('disabled');
1242         },
1243
1244         /**
1245          * Disable the multiselect.
1246          */
1247         disable: function() {
1248             this.$select.prop('disabled', true);
1249             this.$button.prop('disabled', true)
1250                 .addClass('disabled');
1251         },
1252
1253         /**
1254          * Set the options.
1255          *
1256          * @param {Array} options
1257          */
1258         setOptions: function(options) {
1259             this.options = this.mergeOptions(options);
1260         },
1261
1262         /**
1263          * Merges the given options with the default options.
1264          *
1265          * @param {Array} options
1266          * @returns {Array}
1267          */
1268         mergeOptions: function(options) {
1269             return $.extend(true, {}, this.defaults, this.options, options);
1270         },
1271
1272         /**
1273          * Checks whether a select all checkbox is present.
1274          *
1275          * @returns {Boolean}
1276          */
1277         hasSelectAll: function() {
1278             return $('li.multiselect-all', this.$ul).length > 0;
1279         },
1280
1281         /**
1282          * Updates the select all checkbox based on the currently displayed and selected checkboxes.
1283          */
1284         updateSelectAll: function() {
1285             if (this.hasSelectAll()) {
1286                 var allBoxes = $("li:not(.multiselect-item):not(.filter-hidden) input:enabled", this.$ul);
1287                 var allBoxesLength = allBoxes.length;
1288                 var checkedBoxesLength = allBoxes.filter(":checked").length;
1289                 var selectAllLi  = $("li.multiselect-all", this.$ul);
1290                 var selectAllInput = selectAllLi.find("input");
1291                 
1292                 if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) {
1293                     selectAllInput.prop("checked", true);
1294                     selectAllLi.addClass(this.options.selectedClass);
1295                     this.options.onSelectAll();
1296                 }
1297                 else {
1298                     selectAllInput.prop("checked", false);
1299                     selectAllLi.removeClass(this.options.selectedClass);
1300                 }
1301             }
1302         },
1303
1304         /**
1305          * Update the button text and its title based on the currently selected options.
1306          */
1307         updateButtonText: function() {
1308             var options = this.getSelected();
1309             
1310             // First update the displayed button text.
1311             if (this.options.enableHTML) {
1312                 $('.multiselect .multiselect-selected-text', this.$container).html(this.options.buttonText(options, this.$select));
1313             }
1314             else {
1315                 $('.multiselect .multiselect-selected-text', this.$container).text(this.options.buttonText(options, this.$select));
1316             }
1317             
1318             // Now update the title attribute of the button.
1319             $('.multiselect', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
1320         },
1321
1322         /**
1323          * Get all selected options.
1324          *
1325          * @returns {jQUery}
1326          */
1327         getSelected: function() {
1328             return $('option', this.$select).filter(":selected");
1329         },
1330
1331         /**
1332          * Gets a select option by its value.
1333          *
1334          * @param {String} value
1335          * @returns {jQuery}
1336          */
1337         getOptionByValue: function (value) {
1338
1339             var options = $('option', this.$select);
1340             var valueToCompare = value.toString();
1341
1342             for (var i = 0; i < options.length; i = i + 1) {
1343                 var option = options[i];
1344                 if (option.value === valueToCompare) {
1345                     return $(option);
1346                 }
1347             }
1348         },
1349
1350         /**
1351          * Get the input (radio/checkbox) by its value.
1352          *
1353          * @param {String} value
1354          * @returns {jQuery}
1355          */
1356         getInputByValue: function (value) {
1357
1358             var checkboxes = $('li input', this.$ul);
1359             var valueToCompare = value.toString();
1360
1361             for (var i = 0; i < checkboxes.length; i = i + 1) {
1362                 var checkbox = checkboxes[i];
1363                 if (checkbox.value === valueToCompare) {
1364                     return $(checkbox);
1365                 }
1366             }
1367         },
1368
1369         /**
1370          * Used for knockout integration.
1371          */
1372         updateOriginalOptions: function() {
1373             this.originalOptions = this.$select.clone()[0].options;
1374         },
1375
1376         asyncFunction: function(callback, timeout, self) {
1377             var args = Array.prototype.slice.call(arguments, 3);
1378             return setTimeout(function() {
1379                 callback.apply(self || window, args);
1380             }, timeout);
1381         },
1382
1383         setAllSelectedText: function(allSelectedText) {
1384             this.options.allSelectedText = allSelectedText;
1385             this.updateButtonText();
1386         }
1387     };
1388
1389     $.fn.multiselect = function(option, parameter, extraOptions) {
1390         return this.each(function() {
1391             var data = $(this).data('multiselect');
1392             var options = typeof option === 'object' && option;
1393
1394             // Initialize the multiselect.
1395             if (!data) {
1396                 data = new Multiselect(this, options);
1397                 $(this).data('multiselect', data);
1398             }
1399
1400             // Call multiselect method.
1401             if (typeof option === 'string') {
1402                 data[option](parameter, extraOptions);
1403                 
1404                 if (option === 'destroy') {
1405                     $(this).data('multiselect', false);
1406                 }
1407             }
1408         });
1409     };
1410
1411     $.fn.multiselect.Constructor = Multiselect;
1412
1413     $(function() {
1414         $("select[data-role=multiselect]").multiselect();
1415     });
1416
1417 }(window.jQuery);