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