From ebd154df552c1c3a4f86ae6eed1a561360d75f60 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Mon, 18 Apr 2016 07:51:13 -0700 Subject: [PATCH 1/3] Added "setSelection" method to code, tests, and documentation. The "setSelection" method allows an explicit list of options to be set. Deselected options in the provided list will be selected and selection options that are not in the provided list be deselected. Internally, it delegates to the "select" and "deselect" methods and gives the caller the same option for firing the onChange notification. The testing suite has been updated to validate the new feature. The tests make sure that the selection changes as it is supposed to, the onChange notification fires appropriately, and doesn't fire when requested not to do so. Finally, the documentation page has been updated to show how to use this feature with working and tested examples that are based off of the other ones already provided. --- dist/js/bootstrap-multiselect.js | 276 +++++++++------- index.html | 480 +++++++++++++++++---------- tests/spec/bootstrap-multiselect.js | 489 +++++++++++++++++++--------- 3 files changed, 810 insertions(+), 435 deletions(-) diff --git a/dist/js/bootstrap-multiselect.js b/dist/js/bootstrap-multiselect.js index a7c71eab..42e8fe1a 100644 --- a/dist/js/bootstrap-multiselect.js +++ b/dist/js/bootstrap-multiselect.js @@ -1,23 +1,23 @@ /** * 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, @@ -28,7 +28,7 @@ * - 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 @@ -175,12 +175,12 @@ function Multiselect(select, options) { this.$select = $(select); - + // Placeholder via data attributes if (this.$select.attr("data-placeholder")) { options.nonSelectedText = this.$select.data("placeholder"); } - + this.options = this.mergeOptions($.extend({}, options, this.$select.data())); // Initialization. @@ -197,7 +197,7 @@ this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this); this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this); this.options.onInitialized = $.proxy(this.options.onInitialized, this); - + // Build select all if enabled. this.buildContainer(); this.buildButton(); @@ -212,7 +212,7 @@ if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { this.disable(); } - + this.$select.hide().after(this.$container); this.options.onInitialized(this.$select, this.$container); } @@ -224,24 +224,24 @@ * 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 - && (this.disableIfEmpty || select.prop('disabled')) + if (this.disabledText.length > 0 + && (this.disableIfEmpty || select.prop('disabled')) && options.length == 0) { - + return this.disabledText; } else if (options.length === 0) { return this.nonSelectedText; } - else if (this.allSelectedText - && options.length === $('option', $(select)).length - && $('option', $(select)).length !== 1 + else if (this.allSelectedText + && options.length === $('option', $(select)).length + && $('option', $(select)).length !== 1 && this.multiple) { if (this.selectAllNumber) { @@ -257,18 +257,18 @@ 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 - 2); } }, /** * Updates the title of the button similar to the buttonText function. - * + * * @param {jQuery} options * @param {jQuery} select * @returns {@exp;selected@call;substr} @@ -280,7 +280,7 @@ else { var selected = ''; var delimiter = this.delimiterText; - + options.each(function () { var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); selected += label + delimiter; @@ -308,9 +308,9 @@ }, /** * Triggered on change of the multiselect. - * + * * Not triggered when selecting/deselecting options manually. - * + * * @param {jQuery} option * @param {Boolean} checked */ @@ -335,25 +335,25 @@ }, /** * 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(checked) { - + }, /** * Triggered after initializing. @@ -482,12 +482,12 @@ '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', @@ -495,13 +495,13 @@ 'margin-top': "-" + moveCalc + 'px' }); } - + this.$container.append(this.$ul); }, /** * Build the dropdown options and binds all nessecary events. - * + * * Uses createDivider and createOptionValue to create the necessary options. */ buildDropdownOptions: function() { @@ -512,7 +512,7 @@ // Support optgroups and options without a group simultaneously. var tag = $element.prop('tagName') .toLowerCase(); - + if ($element.prop('value') === this.options.selectAllValue) { return; } @@ -597,7 +597,7 @@ // Unselect option. $option.prop('selected', false); } - + // To prevent select all from firing onChange: #575 this.options.onChange($option, checked); } @@ -618,12 +618,12 @@ 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(); @@ -635,39 +635,39 @@ 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; @@ -735,7 +735,7 @@ var allChecked = true; var optionInputs = $visibleOptions.find('input'); var values = []; - + optionInputs.each(function() { allChecked = allChecked && $(this).prop('checked'); values.push($(this).val()); @@ -747,7 +747,7 @@ else { this.deselect(values, false); } - + this.options.onChange(optionInputs, !allChecked); }, this)); } @@ -757,51 +757,51 @@ $("li.multiselect-group", this.$ul).siblings().not("li.multiselect-group, li.multiselect-all", this.$ul).each( function () { $(this).toggleClass('hidden', true); }); - + $("li.multiselect-group", this.$ul).on("click", $.proxy(function(group) { group.stopPropagation(); }, this)); - + $("li.multiselect-group > a > b", this.$ul).on("click", $.proxy(function(t) { t.stopPropagation(); var n = $(t.target).closest('li'); var r = n.nextUntil("li.multiselect-group"); var i = true; - + r.each(function() { i = i && $(this).hasClass('hidden'); }); - + r.toggleClass('hidden', !i); }, this)); - + $("li.multiselect-group > a > input", this.$ul).on("change", $.proxy(function(t) { t.stopPropagation(); var n = $(t.target).closest('li'); var r = n.nextUntil("li.multiselect-group", ':not(.disabled)'); var s = r.find("input"); - + var i = true; s.each(function() { i = i && $(this).prop("checked"); }); - + s.prop("checked", !i).trigger("change"); }, this)); - + // Set the initial selection state of the groups. $('li.multiselect-group', this.$ul).each(function() { var r = $(this).nextUntil("li.multiselect-group", ':not(.disabled)'); var s = r.find("input"); - + var i = true; s.each(function() { i = i && $(this).prop("checked"); }); - + $(this).find('input').prop("checked", i); }); - + // Update the group checkbox based on new selections among the // corresponding children. $("li input", this.$ul).on("change", $.proxy(function(t) { @@ -811,19 +811,19 @@ var r2 = n.nextUntil("li.multiselect-group", ':not(.disabled)'); var s1 = r1.find("input"); var s2 = r2.find("input"); - + var i = $(t.target).prop('checked'); s1.each(function() { i = i && $(this).prop("checked"); }); - + s2.each(function() { i = i && $(this).prop("checked"); }); - + n.prevAll('.multiselect-group').find('input').prop('checked', i); }, this)); - + $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); $("li.multiselect-group > a, 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'); @@ -858,7 +858,7 @@ else { $label.text(" " + label); } - + var $checkbox = $('').attr('type', inputType); if (this.options.checkboxName) { @@ -911,7 +911,7 @@ * * @param {jQuery} group */ - createOptgroup: function(group) { + createOptgroup: function(group) { if (this.options.enableCollapsibleOptGroups && this.options.multiple) { var label = $(group).attr("label"); var value = $(group).attr("value"); @@ -960,14 +960,14 @@ /** * 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 @@ -980,21 +980,21 @@ 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); @@ -1021,7 +1021,7 @@ 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); @@ -1033,7 +1033,7 @@ }, this)); this.$filter.find('.input-group').append(clearBtn); } - + this.$ul.prepend(this.$filter); this.$filter.val(this.query).on('click', function(event) { @@ -1043,7 +1043,7 @@ if (event.which === 13) { event.preventDefault(); } - + // This is useful to catch "keydown" events after the browser has updated the control. clearTimeout(this.searchTimeout); @@ -1091,7 +1091,7 @@ // Toggle current element (group or group item) according to showElement boolean. $(element).toggle(showElement).toggleClass('filter-hidden', !showElement); - + // Differentiate groups and group items. if ($(element).hasClass('multiselect-group')) { // Remember group status. @@ -1103,7 +1103,7 @@ if (showElement) { $(currentGroup).show().removeClass('filter-hidden'); } - + // Show all group items when group name satisfies filter. if (!showElement && currentGroupVisible) { $(element).show().removeClass('filter-hidden'); @@ -1134,7 +1134,7 @@ */ refresh: function () { var inputs = $.map($('li input', this.$ul), $); - + $('option', this.$select).each($.proxy(function (index, element) { var $elem = $(element); var value = $elem.val(); @@ -1181,10 +1181,10 @@ /** * 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 */ @@ -1206,11 +1206,11 @@ if($option === undefined || $checkbox === undefined) { continue; } - + if (!this.options.multiple) { this.deselectAll(false); } - + if (this.options.selectedClass) { $checkbox.closest('li') .addClass(this.options.selectedClass); @@ -1218,7 +1218,7 @@ $checkbox.prop('checked', true); $option.prop('selected', true); - + if (triggerOnChange) { this.options.onChange($option, true); } @@ -1239,10 +1239,10 @@ /** * 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 */ @@ -1272,7 +1272,7 @@ $checkbox.prop('checked', false); $option.prop('selected', false); - + if (triggerOnChange) { this.options.onChange($option, false); } @@ -1281,7 +1281,7 @@ this.updateButtonText(); this.updateSelectAll(); }, - + /** * Selects all enabled & visible options. * @@ -1292,13 +1292,13 @@ */ selectAll: function (justVisible, triggerOnSelectAll) { justVisible = (this.options.enableCollapsibleOptGroups && this.options.multiple) ? false : justVisible; - + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; var allCheckboxes = $("li input[type='checkbox']:enabled", this.$ul); var visibleCheckboxes = allCheckboxes.filter(":visible"); var allCheckboxesCount = allCheckboxes.length; var visibleCheckboxesCount = visibleCheckboxes.length; - + if(justVisible) { visibleCheckboxes.prop('checked', true); $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").addClass(this.options.selectedClass); @@ -1307,7 +1307,7 @@ allCheckboxes.prop('checked', true); $("li:not(.divider):not(.disabled)", this.$ul).addClass(this.options.selectedClass); } - + if (allCheckboxesCount === visibleCheckboxesCount || justVisible === false) { $("option:not([data-role='divider']):enabled", this.$select).prop('selected', true); } @@ -1315,12 +1315,12 @@ var values = visibleCheckboxes.map(function() { return $(this).val(); }).get(); - + $("option:enabled", this.$select).filter(function(index) { return $.inArray($(this).val(), values) !== -1; }).prop('selected', true); } - + if (triggerOnSelectAll) { this.options.onSelectAll(); } @@ -1328,27 +1328,27 @@ /** * Deselects all options. - * + * * If justVisible is true or not specified, only visible options are deselected. - * + * * @param {Boolean} justVisible */ deselectAll: function (justVisible) { justVisible = (this.options.enableCollapsibleOptGroups && this.options.multiple) ? false : justVisible; justVisible = typeof justVisible === 'undefined' ? true : justVisible; - - if(justVisible) { + + if(justVisible) { var visibleCheckboxes = $("li input[type='checkbox']:not(:disabled)", this.$ul).filter(":visible"); visibleCheckboxes.prop('checked', false); - + var values = visibleCheckboxes.map(function() { return $(this).val(); }).get(); - + $("option:enabled", this.$select).filter(function(index) { return $.inArray($(this).val(), values) !== -1; }).prop('selected', false); - + if (this.options.selectedClass) { $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").removeClass(this.options.selectedClass); } @@ -1356,16 +1356,68 @@ else { $("li input[type='checkbox']:enabled", this.$ul).prop('checked', false); $("option:enabled", this.$select).prop('selected', false); - + if (this.options.selectedClass) { $("li:not(.divider):not(.disabled)", this.$ul).removeClass(this.options.selectedClass); } } }, + /** + * Set the current selection + * + * Helper function to set which options are currently selected by invoking + * 'select' for all unselected values that appear in the given list and + * invoking 'deselect' for all selected values that do NOT appear in the + * given list + * + * @param {Array} selection + * @param {Boolean} triggerOnChange + */ + setSelected: function (selection, triggerOnChange) { + if (!$.isArray(selection)) { + selection = [selection]; + } + + // Get all of the enabled options + var opts = $("option:enabled", this.$select); + var selectedOpts = opts.filter(":selected"); + var notSelectedOpts = opts.not(":selected"); + var values; + var val; + + // Find all the not-selected options + values = notSelectedOpts.map(function () { + // If their value is in the array, it should be selected so + // return that value (otherwise, return null to omit it from the result) + val = $(this).val(); + return ($.inArray(val, selection) > -1) ? val : null; + }).get(); + + if ( values.length ) + { + // Select all that need to be selected + this.select(values, triggerOnChange); + } + + // Find all the selected options + values = selectedOpts.map(function () { + // If their value is NOT in the array, it should be deselected so + // return that value (otherwise, return null to omit it from the result) + val = $(this).val(); + return ($.inArray(val, selection) == -1) ? val : null; + }).get(); + + if ( values.length ) + { + // Deselect all that should not be selected + this.deselect(values, triggerOnChange); + } + }, + /** * Rebuild the plugin. - * + * * Rebuilds the dropdown, the filter and the select all option. */ rebuild: function() { @@ -1380,14 +1432,14 @@ this.updateButtonText(); this.updateSelectAll(true); - + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { this.disable(); } else { this.enable(); } - + if (this.options.dropRight) { this.$ul.addClass('pull-right'); } @@ -1397,21 +1449,21 @@ * 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 $tag.append($('