/** * bootstrap multiselect (https://github.com/davidstutz/bootstrap-multiselect) * * apache license, version 2.0: * copyright (c) 2012 - 2015 david stutz * * licensed under the apache license, version 2.0 (the "license"); you may not * use this file except in compliance with the license. you may obtain a * copy of the license at http://www.apache.org/licenses/license-2.0 * * unless required by applicable law or agreed to in writing, software * distributed under the license is distributed on an "as is" basis, without * warranties or conditions of any kind, either express or implied. see the * license for the specific language governing permissions and limitations * under the license. * * bsd 3-clause license: * copyright (c) 2012 - 2015 david stutz * all rights reserved. * * redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - neither the name of david stutz nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * this software is provided by the copyright holders and contributors "as is" * and any express or implied warranties, including, but not limited to, * the implied warranties of merchantability and fitness for a particular * purpose are disclaimed. in no event shall the copyright holder or * contributors be liable for any direct, indirect, incidental, special, * exemplary, or consequential damages (including, but not limited to, * procurement of substitute goods or services; loss of use, data, or profits; * or business interruption) however caused and on any theory of liability, * whether in contract, strict liability, or tort (including negligence or * otherwise) arising in any way out of the use of this software, even if * advised of the possibility of such damage. */ !function ($) { "use strict";// jshint ;_; if (typeof ko !== 'undefined' && ko.bindinghandlers && !ko.bindinghandlers.multiselect) { ko.bindinghandlers.multiselect = { after: ['options', 'value', 'selectedoptions', 'enable', 'disable'], init: function(element, valueaccessor, allbindings, viewmodel, bindingcontext) { var $element = $(element); var config = ko.tojs(valueaccessor()); $element.multiselect(config); if (allbindings.has('options')) { var options = allbindings.get('options'); if (ko.isobservable(options)) { ko.computed({ read: function() { options(); settimeout(function() { var ms = $element.data('multiselect'); if (ms) ms.updateoriginaloptions();//not sure how beneficial this is. $element.multiselect('rebuild'); }, 1); }, disposewhennodeisremoved: element }); } } //value and selectedoptions are two-way, so these will be triggered even by our own actions. //it needs some way to tell if they are triggered because of us or because of outside change. //it doesn't loop but it's a waste of processing. if (allbindings.has('value')) { var value = allbindings.get('value'); if (ko.isobservable(value)) { ko.computed({ read: function() { value(); settimeout(function() { $element.multiselect('refresh'); }, 1); }, disposewhennodeisremoved: element }).extend({ ratelimit: 100, notifywhenchangesstop: true }); } } //switched from arraychange subscription to general subscription using 'refresh'. //not sure performance is any better using 'select' and 'deselect'. if (allbindings.has('selectedoptions')) { var selectedoptions = allbindings.get('selectedoptions'); if (ko.isobservable(selectedoptions)) { ko.computed({ read: function() { selectedoptions(); settimeout(function() { $element.multiselect('refresh'); }, 1); }, disposewhennodeisremoved: element }).extend({ ratelimit: 100, notifywhenchangesstop: true }); } } var setenabled = function (enable) { settimeout(function () { if (enable) $element.multiselect('enable'); else $element.multiselect('disable'); }); }; if (allbindings.has('enable')) { var enable = allbindings.get('enable'); if (ko.isobservable(enable)) { ko.computed({ read: function () { setenabled(enable()); }, disposewhennodeisremoved: element }).extend({ ratelimit: 100, notifywhenchangesstop: true }); } else { setenabled(enable); } } if (allbindings.has('disable')) { var disable = allbindings.get('disable'); if (ko.isobservable(disable)) { ko.computed({ read: function () { setenabled(!disable()); }, disposewhennodeisremoved: element }).extend({ ratelimit: 100, notifywhenchangesstop: true }); } else { setenabled(!disable); } } ko.utils.domnodedisposal.adddisposecallback(element, function() { $element.multiselect('destroy'); }); }, update: function(element, valueaccessor, allbindings, viewmodel, bindingcontext) { var $element = $(element); var config = ko.tojs(valueaccessor()); $element.multiselect('setoptions', config); $element.multiselect('rebuild'); } }; } function foreach(array, callback) { for (var index = 0; index < array.length; ++index) { callback(array[index], index); } } /** * constructor to create a new multiselect using the given select. * * @param {jquery} select * @param {object} options * @returns {multiselect} */ function multiselect(select, options) { this.$select = $(select); this.options = this.mergeoptions($.extend({}, options, this.$select.data())); // placeholder via data attributes if (this.$select.attr("data-placeholder")) { this.options.nonselectedtext = this.$select.data("placeholder"); } // initialization. // we have to clone to create a new reference. this.originaloptions = this.$select.clone()[0].options; this.query = ''; this.searchtimeout = null; this.lasttoggledinput = null; this.options.multiple = this.$select.attr('multiple') === "multiple"; this.options.onchange = $.proxy(this.options.onchange, this); this.options.onselectall = $.proxy(this.options.onselectall, this); this.options.ondeselectall = $.proxy(this.options.ondeselectall, this); this.options.ondropdownshow = $.proxy(this.options.ondropdownshow, this); this.options.ondropdownhide = $.proxy(this.options.ondropdownhide, this); this.options.ondropdownshown = $.proxy(this.options.ondropdownshown, this); this.options.ondropdownhidden = $.proxy(this.options.ondropdownhidden, this); this.options.oninitialized = $.proxy(this.options.oninitialized, this); this.options.onfiltering = $.proxy(this.options.onfiltering, this); // build select all if enabled. this.buildcontainer(); this.buildbutton(); this.builddropdown(); this.buildselectall(); this.builddropdownoptions(); this.buildfilter(); this.updatebuttontext(); this.updateselectall(true); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } this.options.wasdisabled = this.$select.prop('disabled'); if (this.options.disableifempty && $('option', this.$select).length <= 0) { this.disable(); } this.$select.wrap('').after(this.$container); this.options.oninitialized(this.$select, this.$container); } multiselect.prototype = { defaults: { /** * default text function will either print 'none selected' in case no * option is selected or a list of the selected options up to a length * of 3 selected options. * * @param {jquery} options * @param {jquery} select * @returns {string} */ buttontext: function(options, select) { if (this.disabledtext.length > 0 && (select.prop('disabled') || (options.length == 0 && this.disableifempty))) { return this.disabledtext; } else if (options.length === 0) { return this.nonselectedtext; } else if (this.allselectedtext && options.length === $('option', $(select)).length && $('option', $(select)).length !== 1 && this.multiple) { if (this.selectallnumber) { return this.allselectedtext + ' (' + options.length + ')'; } else { return this.allselectedtext; } } else if (options.length > this.numberdisplayed) { return options.length + ' ' + this.nselectedtext; } else { var selected = ''; var delimiter = this.delimitertext; options.each(function() { var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); selected += label + delimiter; }); return selected.substr(0, selected.length - this.delimitertext.length); } }, /** * updates the title of the button similar to the buttontext function. * * @param {jquery} options * @param {jquery} select * @returns {@exp;selected@call;substr} */ buttontitle: function(options, select) { if (options.length === 0) { return this.nonselectedtext; } else { var selected = ''; var delimiter = this.delimitertext; options.each(function () { var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); selected += label + delimiter; }); return selected.substr(0, selected.length - this.delimitertext.length); } }, checkboxname: function(option) { return false; // no checkbox name }, /** * create a label. * * @param {jquery} element * @returns {string} */ optionlabel: function(element){ return $(element).attr('label') || $(element).text(); }, /** * create a class. * * @param {jquery} element * @returns {string} */ optionclass: function(element) { return $(element).attr('class') || ''; }, /** * triggered on change of the multiselect. * * not triggered when selecting/deselecting options manually. * * @param {jquery} option * @param {boolean} checked */ onchange : function(option, checked) { }, /** * triggered when the dropdown is shown. * * @param {jquery} event */ ondropdownshow: function(event) { }, /** * triggered when the dropdown is hidden. * * @param {jquery} event */ ondropdownhide: function(event) { }, /** * triggered after the dropdown is shown. * * @param {jquery} event */ ondropdownshown: function(event) { }, /** * triggered after the dropdown is hidden. * * @param {jquery} event */ ondropdownhidden: function(event) { }, /** * triggered on select all. */ onselectall: function() { }, /** * triggered on deselect all. */ ondeselectall: function() { }, /** * triggered after initializing. * * @param {jquery} $select * @param {jquery} $container */ oninitialized: function($select, $container) { }, /** * triggered on filtering. * * @param {jquery} $filter */ onfiltering: function($filter) { }, enablehtml: false, buttonclass: 'btn btn-default', inheritclass: false, buttonwidth: 'auto', buttoncontainer: '
', dropright: false, dropup: false, selectedclass: 'active', // maximum height of the dropdown menu. // if maximum height is exceeded a scrollbar will be displayed. maxheight: false, includeselectalloption: false, includeselectallifmorethan: 0, selectalltext: ' select all', selectallvalue: 'multiselect-all', selectallname: false, selectallnumber: true, selectalljustvisible: true, enablefiltering: false, enablecaseinsensitivefiltering: false, enablefullvaluefiltering: false, enableclickableoptgroups: false, enablecollapsibleoptgroups: false, filterplaceholder: 'search', // possible options: 'text', 'value', 'both' filterbehavior: 'text', includefilterclearbtn: true, preventinputchangeevent: false, nonselectedtext: 'none selected', nselectedtext: 'selected', allselectedtext: 'all selected', numberdisplayed: 3, disableifempty: false, disabledtext: '', delimitertext: ', ', templates: { button: '', ul: '', filter: '
  • ', filterclearbtn: '', li: '
  • ', divider: '
  • ', ligroup: '
  • ' } }, constructor: multiselect, /** * builds the container of the multiselect. */ buildcontainer: function() { this.$container = $(this.options.buttoncontainer); this.$container.on('show.bs.dropdown', this.options.ondropdownshow); this.$container.on('hide.bs.dropdown', this.options.ondropdownhide); this.$container.on('shown.bs.dropdown', this.options.ondropdownshown); this.$container.on('hidden.bs.dropdown', this.options.ondropdownhidden); }, /** * builds the button of the multiselect. */ buildbutton: function() { this.$button = $(this.options.templates.button).addclass(this.options.buttonclass); if (this.$select.attr('class') && this.options.inheritclass) { this.$button.addclass(this.$select.attr('class')); } // adopt active state. if (this.$select.prop('disabled')) { this.disable(); } else { this.enable(); } // manually add button width if set. if (this.options.buttonwidth && this.options.buttonwidth !== 'auto') { this.$button.css({ 'width' : '100%', //this.options.buttonwidth, 'overflow' : 'hidden', 'text-overflow' : 'ellipsis' }); this.$container.css({ 'width': this.options.buttonwidth }); } // keep the tab index from the select. var tabindex = this.$select.attr('tabindex'); if (tabindex) { this.$button.attr('tabindex', tabindex); } this.$container.prepend(this.$button); }, /** * builds the ul representing the dropdown menu. */ builddropdown: function() { // build ul. this.$ul = $(this.options.templates.ul); if (this.options.dropright) { this.$ul.addclass('pull-right'); } // set max height of dropdown menu to activate auto scrollbar. if (this.options.maxheight) { // todo: add a class for this option to move the css declarations. this.$ul.css({ 'max-height': this.options.maxheight + 'px', 'overflow-y': 'auto', 'overflow-x': 'hidden' }); } if (this.options.dropup) { var height = math.min(this.options.maxheight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeselectalloption ? 26 : 0) + (this.options.enablefiltering || this.options.enablecaseinsensitivefiltering ? 44 : 0)); var movecalc = height + 34; this.$ul.css({ 'max-height': height + 'px', 'overflow-y': 'auto', 'overflow-x': 'hidden', 'margin-top': "-" + movecalc + 'px' }); } this.$container.append(this.$ul); }, /** * build the dropdown options and binds all necessary events. * * uses createdivider and createoptionvalue to create the necessary options. */ builddropdownoptions: function() { this.$select.children().each($.proxy(function(index, element) { var $element = $(element); // support optgroups and options without a group simultaneously. var tag = $element.prop('tagname') .tolowercase(); if ($element.prop('value') === this.options.selectallvalue) { return; } if (tag === 'optgroup') { this.createoptgroup(element); } else if (tag === 'option') { if ($element.data('role') === 'divider') { this.createdivider(); } else { this.createoptionvalue(element); } } // other illegal tags will be ignored. }, this)); // bind the change event on the dropdown elements. $('li:not(.multiselect-group) input', this.$ul).on('change', $.proxy(function(event) { var $target = $(event.target); var checked = $target.prop('checked') || false; var isselectalloption = $target.val() === this.options.selectallvalue; // apply or unapply the configured selected class. if (this.options.selectedclass) { if (checked) { $target.closest('li') .addclass(this.options.selectedclass); } else { $target.closest('li') .removeclass(this.options.selectedclass); } } // get the corresponding option. var value = $target.val(); var $option = this.getoptionbyvalue(value); var $optionsnotthis = $('option', this.$select).not($option); var $checkboxesnotthis = $('input', this.$container).not($target); if (isselectalloption) { if (checked) { this.selectall(this.options.selectalljustvisible, true); } else { this.deselectall(this.options.selectalljustvisible, true); } } else { if (checked) { $option.prop('selected', true); if (this.options.multiple) { // simply select additional option. $option.prop('selected', true); } else { // unselect all other options and corresponding checkboxes. if (this.options.selectedclass) { $($checkboxesnotthis).closest('li').removeclass(this.options.selectedclass); } $($checkboxesnotthis).prop('checked', false); $optionsnotthis.prop('selected', false); // it's a single selection, so close. this.$button.click(); } if (this.options.selectedclass === "active") { $optionsnotthis.closest("a").css("outline", ""); } } else { // unselect option. $option.prop('selected', false); } // to prevent select all from firing onchange: #575 this.options.onchange($option, checked); // do not update select all or optgroups on select all change! this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } } this.$select.change(); this.updatebuttontext(); if(this.options.preventinputchangeevent) { return false; } }, this)); $('li a', this.$ul).on('mousedown', function(e) { if (e.shiftkey) { // prevent selecting text by shift+click return false; } }); $('li a', this.$ul).on('touchstart click', $.proxy(function(event) { event.stoppropagation(); var $target = $(event.target); if (event.shiftkey && this.options.multiple) { if($target.is("label")){ // handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) event.preventdefault(); $target = $target.find("input"); $target.prop("checked", !$target.prop("checked")); } var checked = $target.prop('checked') || false; if (this.lasttoggledinput !== null && this.lasttoggledinput !== $target) { // make sure we actually have a range var from = $target.closest("li").index(); var to = this.lasttoggledinput.closest("li").index(); if (from > to) { // swap the indices var tmp = to; to = from; from = tmp; } // make sure we grab all elements since slice excludes the last index ++to; // change the checkboxes and underlying options var range = this.$ul.find("li").slice(from, to).find("input"); range.prop('checked', checked); if (this.options.selectedclass) { range.closest('li') .toggleclass(this.options.selectedclass, checked); } for (var i = 0, j = range.length; i < j; i++) { var $checkbox = $(range[i]); var $option = this.getoptionbyvalue($checkbox.val()); $option.prop('selected', checked); } } // trigger the select "change" event $target.trigger("change"); } // remembers last clicked option if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ this.lasttoggledinput = $target; } $target.blur(); }, this)); // keyboard support. this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { if ($('input[type="text"]', this.$container).is(':focus')) { return; } if (event.keycode === 9 && this.$container.hasclass('open')) { this.$button.click(); } else { var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible"); if (!$items.length) { return; } var index = $items.index($items.filter(':focus')); // navigation up. if (event.keycode === 38 && index > 0) { index--; } // navigate down. else if (event.keycode === 40 && index < $items.length - 1) { index++; } else if (!~index) { index = 0; } var $current = $items.eq(index); $current.focus(); if (event.keycode === 32 || event.keycode === 13) { var $checkbox = $current.find('input'); $checkbox.prop("checked", !$checkbox.prop("checked")); $checkbox.change(); } event.stoppropagation(); event.preventdefault(); } }, this)); if (this.options.enableclickableoptgroups && this.options.multiple) { $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { event.stoppropagation(); var $target = $(event.target); var checked = $target.prop('checked') || false; var $li = $(event.target).closest('li'); var $group = $li.nextuntil("li.multiselect-group") .not('.multiselect-filter-hidden') .not('.disabled'); var $inputs = $group.find("input"); var values = []; var $options = []; if (this.options.selectedclass) { if (checked) { $li.addclass(this.options.selectedclass); } else { $li.removeclass(this.options.selectedclass); } } $.each($inputs, $.proxy(function(index, input) { var value = $(input).val(); var $option = this.getoptionbyvalue(value); if (checked) { $(input).prop('checked', true); $(input).closest('li') .addclass(this.options.selectedclass); $option.prop('selected', true); } else { $(input).prop('checked', false); $(input).closest('li') .removeclass(this.options.selectedclass); $option.prop('selected', false); } $options.push(this.getoptionbyvalue(value)); }, this)) // cannot use select or deselect here because it would call updateoptgroups again. this.options.onchange($options, checked); this.updatebuttontext(); this.updateselectall(); }, this)); } if (this.options.enablecollapsibleoptgroups && this.options.multiple) { $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { var $li = $(event.target).closest('li'); var $inputs = $li.nextuntil("li.multiselect-group") .not('.multiselect-filter-hidden'); var visible = true; $inputs.each(function() { visible = visible && $(this).is(':visible'); }); if (visible) { $inputs.hide() .addclass('multiselect-collapsible-hidden'); } else { $inputs.show() .removeclass('multiselect-collapsible-hidden'); } }, this)); $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); } }, /** * create an option using the given select option. * * @param {jquery} element */ createoptionvalue: function(element) { var $element = $(element); if ($element.is(':selected')) { $element.prop('selected', true); } // support the label attribute on options. var label = this.options.optionlabel(element); var classes = this.options.optionclass(element); var value = $element.val(); var inputtype = this.options.multiple ? "checkbox" : "radio"; var $li = $(this.options.templates.li); var $label = $('label', $li); $label.addclass(inputtype); $li.addclass(classes); if (this.options.enablehtml) { $label.html(" " + label); } else { $label.text(" " + label); } var $checkbox = $('').attr('type', inputtype); var name = this.options.checkboxname($element); if (name) { $checkbox.attr('name', name); } $label.prepend($checkbox); var selected = $element.prop('selected') || false; $checkbox.val(value); if (value === this.options.selectallvalue) { $li.addclass("multiselect-item multiselect-all"); $checkbox.parent().parent() .addclass('multiselect-all'); } $label.attr('title', $element.attr('title')); this.$ul.append($li); if ($element.is(':disabled')) { $checkbox.attr('disabled', 'disabled') .prop('disabled', true) .closest('a') .attr("tabindex", "-1") .closest('li') .addclass('disabled'); } $checkbox.prop('checked', selected); if (selected && this.options.selectedclass) { $checkbox.closest('li') .addclass(this.options.selectedclass); } }, /** * creates a divider using the given select option. * * @param {jquery} element */ createdivider: function(element) { var $divider = $(this.options.templates.divider); this.$ul.append($divider); }, /** * creates an optgroup. * * @param {jquery} group */ createoptgroup: function(group) { var label = $(group).attr("label"); var value = $(group).attr("value"); var $li = $('
  • '); var classes = this.options.optionclass(group); $li.addclass(classes); if (this.options.enablehtml) { $('label b', $li).html(" " + label); } else { $('label b', $li).text(" " + label); } if (this.options.enablecollapsibleoptgroups && this.options.multiple) { $('a', $li).append(''); } if (this.options.enableclickableoptgroups && this.options.multiple) { $('a label', $li).prepend(''); } if ($(group).is(':disabled')) { $li.addclass('disabled'); } this.$ul.append($li); $("option", group).each($.proxy(function($, group) { this.createoptionvalue(group); }, this)) }, /** * build the select all. * * checks if a select all has already been created. */ buildselectall: function() { if (typeof this.options.selectallvalue === 'number') { this.options.selectallvalue = this.options.selectallvalue.tostring(); } var alreadyhasselectall = this.hasselectall(); if (!alreadyhasselectall && this.options.includeselectalloption && this.options.multiple && $('option', this.$select).length > this.options.includeselectallifmorethan) { // check whether to add a divider after the select all. if (this.options.includeselectalldivider) { this.$ul.prepend($(this.options.templates.divider)); } var $li = $(this.options.templates.li); $('label', $li).addclass("checkbox"); if (this.options.enablehtml) { $('label', $li).html(" " + this.options.selectalltext); } else { $('label', $li).text(" " + this.options.selectalltext); } if (this.options.selectallname) { $('label', $li).prepend(''); } else { $('label', $li).prepend(''); } var $checkbox = $('input', $li); $checkbox.val(this.options.selectallvalue); $li.addclass("multiselect-item multiselect-all"); $checkbox.parent().parent() .addclass('multiselect-all'); this.$ul.prepend($li); $checkbox.prop('checked', false); } }, /** * builds the filter. */ buildfilter: function() { // build filter if filtering or case insensitive filtering is enabled and the number of options exceeds (or equals) enablefilterlength. if (this.options.enablefiltering || this.options.enablecaseinsensitivefiltering) { var enablefilterlength = math.max(this.options.enablefiltering, this.options.enablecaseinsensitivefiltering); if (this.$select.find('option').length >= enablefilterlength) { this.$filter = $(this.options.templates.filter); $('input', this.$filter).attr('placeholder', this.options.filterplaceholder); // adds optional filter clear button if(this.options.includefilterclearbtn) { var clearbtn = $(this.options.templates.filterclearbtn); clearbtn.on('click', $.proxy(function(event){ cleartimeout(this.searchtimeout); this.$filter.find('.multiselect-search').val(''); $('li', this.$ul).show().removeclass('multiselect-filter-hidden'); this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } }, this)); this.$filter.find('.input-group').append(clearbtn); } this.$ul.prepend(this.$filter); this.$filter.val(this.query).on('click', function(event) { event.stoppropagation(); }).on('input keydown', $.proxy(function(event) { // cancel enter key default behaviour if (event.which === 13) { event.preventdefault(); } // this is useful to catch "keydown" events after the browser has updated the control. cleartimeout(this.searchtimeout); this.searchtimeout = this.asyncfunction($.proxy(function() { if (this.query !== event.target.value) { this.query = event.target.value; var currentgroup, currentgroupvisible; $.each($('li', this.$ul), $.proxy(function(index, element) { var value = $('input', element).length > 0 ? $('input', element).val() : ""; var text = $('label', element).text(); var filtercandidate = ''; if ((this.options.filterbehavior === 'text')) { filtercandidate = text; } else if ((this.options.filterbehavior === 'value')) { filtercandidate = value; } else if (this.options.filterbehavior === 'both') { filtercandidate = text + '\n' + value; } if (value !== this.options.selectallvalue && text) { // by default lets assume that element is not // interesting for this search. var showelement = false; if (this.options.enablecaseinsensitivefiltering) { filtercandidate = filtercandidate.tolowercase(); this.query = this.query.tolowercase(); } if (this.options.enablefullvaluefiltering && this.options.filterbehavior !== 'both') { var valuetomatch = filtercandidate.trim().substring(0, this.query.length); if (this.query.indexof(valuetomatch) > -1) { showelement = true; } } else if (filtercandidate.indexof(this.query) > -1) { showelement = true; } // toggle current element (group or group item) according to showelement boolean. $(element).toggle(showelement) .toggleclass('multiselect-filter-hidden', !showelement); // differentiate groups and group items. if ($(element).hasclass('multiselect-group')) { // remember group status. currentgroup = element; currentgroupvisible = showelement; } else { // show group name when at least one of its items is visible. if (showelement) { $(currentgroup).show() .removeclass('multiselect-filter-hidden'); } // show all group items when group name satisfies filter. if (!showelement && currentgroupvisible) { $(element).show() .removeclass('multiselect-filter-hidden'); } } } }, this)); } this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } this.options.onfiltering(event.target); }, this), 300, this); }, this)); } } }, /** * unbinds the whole plugin. */ destroy: function() { this.$container.remove(); this.$select.show(); // reset original state this.$select.prop('disabled', this.options.wasdisabled); this.$select.data('multiselect', null); }, /** * refreshs the multiselect based on the selected options of the select. */ refresh: function () { var inputs = $.map($('li input', this.$ul), $); $('option', this.$select).each($.proxy(function (index, element) { var $elem = $(element); var value = $elem.val(); var $input; for (var i = inputs.length; 0 < i--; /**/) { if (value !== ($input = inputs[i]).val()) continue; // wrong li if ($elem.is(':selected')) { $input.prop('checked', true); if (this.options.selectedclass) { $input.closest('li') .addclass(this.options.selectedclass); } } else { $input.prop('checked', false); if (this.options.selectedclass) { $input.closest('li') .removeclass(this.options.selectedclass); } } if ($elem.is(":disabled")) { $input.attr('disabled', 'disabled') .prop('disabled', true) .closest('li') .addclass('disabled'); } else { $input.prop('disabled', false) .closest('li') .removeclass('disabled'); } break; // assumes unique values } }, this)); this.updatebuttontext(); this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } }, /** * select all options of the given values. * * if triggeronchange is set to true, the on change event is triggered if * and only if one value is passed. * * @param {array} selectvalues * @param {boolean} triggeronchange */ select: function(selectvalues, triggeronchange) { if(!$.isarray(selectvalues)) { selectvalues = [selectvalues]; } for (var i = 0; i < selectvalues.length; i++) { var value = selectvalues[i]; if (value === null || value === undefined) { continue; } var $option = this.getoptionbyvalue(value); var $checkbox = this.getinputbyvalue(value); if($option === undefined || $checkbox === undefined) { continue; } if (!this.options.multiple) { this.deselectall(false); } if (this.options.selectedclass) { $checkbox.closest('li') .addclass(this.options.selectedclass); } $checkbox.prop('checked', true); $option.prop('selected', true); if (triggeronchange) { this.options.onchange($option, true); } } this.updatebuttontext(); this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } }, /** * clears all selected items. */ clearselection: function () { this.deselectall(false); this.updatebuttontext(); this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } }, /** * deselects all options of the given values. * * if triggeronchange is set to true, the on change event is triggered, if * and only if one value is passed. * * @param {array} deselectvalues * @param {boolean} triggeronchange */ deselect: function(deselectvalues, triggeronchange) { if(!$.isarray(deselectvalues)) { deselectvalues = [deselectvalues]; } for (var i = 0; i < deselectvalues.length; i++) { var value = deselectvalues[i]; if (value === null || value === undefined) { continue; } var $option = this.getoptionbyvalue(value); var $checkbox = this.getinputbyvalue(value); if($option === undefined || $checkbox === undefined) { continue; } if (this.options.selectedclass) { $checkbox.closest('li') .removeclass(this.options.selectedclass); } $checkbox.prop('checked', false); $option.prop('selected', false); if (triggeronchange) { this.options.onchange($option, false); } } this.updatebuttontext(); this.updateselectall(); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } }, /** * selects all enabled & visible options. * * if justvisible is true or not specified, only visible options are selected. * * @param {boolean} justvisible * @param {boolean} triggeronselectall */ selectall: function (justvisible, triggeronselectall) { var justvisible = typeof justvisible === 'undefined' ? true : justvisible; var alllis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); var visiblelis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); if(justvisible) { $('input:enabled' , visiblelis).prop('checked', true); visiblelis.addclass(this.options.selectedclass); $('input:enabled' , visiblelis).each($.proxy(function(index, element) { var value = $(element).val(); var option = this.getoptionbyvalue(value); $(option).prop('selected', true); }, this)); } else { $('input:enabled' , alllis).prop('checked', true); alllis.addclass(this.options.selectedclass); $('input:enabled' , alllis).each($.proxy(function(index, element) { var value = $(element).val(); var option = this.getoptionbyvalue(value); $(option).prop('selected', true); }, this)); } $('li input[value="' + this.options.selectallvalue + '"]', this.$ul).prop('checked', true); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } if (triggeronselectall) { this.options.onselectall(); } }, /** * deselects all options. * * if justvisible is true or not specified, only visible options are deselected. * * @param {boolean} justvisible */ deselectall: function (justvisible, triggerondeselectall) { var justvisible = typeof justvisible === 'undefined' ? true : justvisible; var alllis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); var visiblelis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); if(justvisible) { $('input[type="checkbox"]:enabled' , visiblelis).prop('checked', false); visiblelis.removeclass(this.options.selectedclass); $('input[type="checkbox"]:enabled' , visiblelis).each($.proxy(function(index, element) { var value = $(element).val(); var option = this.getoptionbyvalue(value); $(option).prop('selected', false); }, this)); } else { $('input[type="checkbox"]:enabled' , alllis).prop('checked', false); alllis.removeclass(this.options.selectedclass); $('input[type="checkbox"]:enabled' , alllis).each($.proxy(function(index, element) { var value = $(element).val(); var option = this.getoptionbyvalue(value); $(option).prop('selected', false); }, this)); } $('li input[value="' + this.options.selectallvalue + '"]', this.$ul).prop('checked', false); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } if (triggerondeselectall) { this.options.ondeselectall(); } }, /** * rebuild the plugin. * * rebuilds the dropdown, the filter and the select all option. */ rebuild: function() { this.$ul.html(''); // important to distinguish between radios and checkboxes. this.options.multiple = this.$select.attr('multiple') === "multiple"; this.buildselectall(); this.builddropdownoptions(); this.buildfilter(); this.updatebuttontext(); this.updateselectall(true); if (this.options.enableclickableoptgroups && this.options.multiple) { this.updateoptgroups(); } if (this.options.disableifempty && $('option', this.$select).length <= 0) { this.disable(); } else { this.enable(); } if (this.options.dropright) { this.$ul.addclass('pull-right'); } }, /** * the provided data will be used to build the dropdown. */ dataprovider: function(dataprovider) { var groupcounter = 0; var $select = this.$select.empty(); $.each(dataprovider, function (index, option) { var $tag; if ($.isarray(option.children)) { // create optiongroup tag groupcounter++; $tag = $('').attr({ label: option.label || 'group ' + groupcounter, disabled: !!option.disabled }); foreach(option.children, function(suboption) { // add children option tags var attributes = { value: suboption.value, label: suboption.label || suboption.value, title: suboption.title, selected: !!suboption.selected, disabled: !!suboption.disabled }; //loop through attributes object and add key-value for each attribute for (var key in suboption.attributes) { attributes['data-' + key] = suboption.attributes[key]; } //append original attributes + new data attributes to option $tag.append($('