vendor: 14,883 bytes, lines 1-358
1/** 2 * Bootstrap Multiselect (http://davidstutz.de/bootstrap-multiselect/) 3 * 4 * Apache License, Version 2.0: 5 * Copyright (c) 2012 - 2018 David Stutz 6 * 7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 8 * use this file except in compliance with the License. You may obtain a 9 * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 * License for the specific language governing permissions and limitations 15 * under the License. 16 * 17 * BSD 3-Clause License: 18 * Copyright (c) 2012 - 2018 David Stutz 19 * All rights reserved. 20 * 21 * Redistribution and use in source and binary forms, with or without 22 * modification, are permitted provided that the following conditions are met: 23 * - Redistributions of source code must retain the above copyright notice, 24 * this list of conditions and the following disclaimer. 25 * - Redistributions in binary form must reproduce the above copyright notice, 26 * this list of conditions and the following disclaimer in the documentation 27 * and/or other materials provided with the distribution. 28 * - Neither the name of David Stutz nor the names of its contributors may be 29 * used to endorse or promote products derived from this software without 30 * specific prior written permission. 31 * 32 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 33 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 34 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 35 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 36 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 37 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 38 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 39 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 40 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 41 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 42 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 */ 44(function (root, factory) { 45 // check to see if 'knockout' AMD module is specified if using requirejs 46 if (typeof define === 'function' && define.amd && 47 typeof require === 'function' && typeof require.specified === 'function' && require.specified('knockout')) { 48 49 // AMD. Register as an anonymous module. 50 define(['jquery', 'knockout'], factory); 51 } else { 52 // Browser globals 53 factory(root.jQuery, root.ko); 54 } 55})(this, function ($, ko) { 56 "use strict";// jshint ;_; 57 58 if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) { 59 ko.bindingHandlers.multiselect = { 60 after: ['options', 'value', 'selectedOptions', 'enable', 'disable'], 61 62 init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { 63 var $element = $(element); 64 var config = ko.toJS(valueAccessor()); 65 66 $element.multiselect(config); 67 68 if (allBindings.has('options')) { 69 var options = allBindings.get('options'); 70 if (ko.isObservable(options)) { 71 ko.computed({ 72 read: function() { 73 options(); 74 setTimeout(function() { 75 var ms = $element.data('multiselect'); 76 if (ms) 77 ms.updateOriginalOptions();//Not sure how beneficial this is. 78 $element.multiselect('rebuild'); 79 }, 1); 80 }, 81 disposeWhenNodeIsRemoved: element 82 }); 83 } 84 } 85 86 //value and selectedOptions are two-way, so these will be triggered even by our own actions. 87 //It needs some way to tell if they are triggered because of us or because of outside change. 88 //It doesn't loop but it's a waste of processing. 89 if (allBindings.has('value')) { 90 var value = allBindings.get('value'); 91 if (ko.isObservable(value)) { 92 ko.computed({ 93 read: function() { 94 value(); 95 setTimeout(function() { 96 $element.multiselect('refresh'); 97 }, 1); 98 }, 99 disposeWhenNodeIsRemoved: element 100 }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); 101 } 102 } 103 104 //Switched from arrayChange subscription to general subscription using 'refresh'. 105 //Not sure performance is any better using 'select' and 'deselect'. 106 if (allBindings.has('selectedOptions')) { 107 var selectedOptions = allBindings.get('selectedOptions'); 108 if (ko.isObservable(selectedOptions)) { 109 ko.computed({ 110 read: function() { 111 selectedOptions(); 112 setTimeout(function() { 113 $element.multiselect('refresh'); 114 }, 1); 115 }, 116 disposeWhenNodeIsRemoved: element 117 }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); 118 } 119 } 120 121 var setEnabled = function (enable) { 122 setTimeout(function () { 123 if (enable) 124 $element.multiselect('enable'); 125 else 126 $element.multiselect('disable'); 127 }); 128 }; 129 130 if (allBindings.has('enable')) { 131 var enable = allBindings.get('enable'); 132 if (ko.isObservable(enable)) { 133 ko.computed({ 134 read: function () { 135 setEnabled(enable()); 136 }, 137 disposeWhenNodeIsRemoved: element 138 }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); 139 } else { 140 setEnabled(enable); 141 } 142 } 143 144 if (allBindings.has('disable')) { 145 var disable = allBindings.get('disable'); 146 if (ko.isObservable(disable)) { 147 ko.computed({ 148 read: function () { 149 setEnabled(!disable()); 150 }, 151 disposeWhenNodeIsRemoved: element 152 }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); 153 } else { 154 setEnabled(!disable); 155 } 156 } 157 158 ko.utils.domNodeDisposal.addDisposeCallback(element, function() { 159 $element.multiselect('destroy'); 160 }); 161 }, 162 163 update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { 164 var $element = $(element); 165 var config = ko.toJS(valueAccessor()); 166 167 $element.multiselect('setOptions', config); 168 $element.multiselect('rebuild'); 169 } 170 }; 171 } 172 173 function forEach(array, callback) { 174 for (var index = 0; index < array.length; ++index) { 175 callback(array[index], index); 176 } 177 } 178 179 /** 180 * Constructor to create a new multiselect using the given select. 181 * 182 * @param {jQuery} select 183 * @param {Object} options 184 * @returns {Multiselect} 185 */ 186 function Multiselect(select, options) { 187 188 this.$select = $(select); 189 this.options = this.mergeOptions($.extend({}, options, this.$select.data())); 190 191 // Placeholder via data attributes 192 if (this.$select.attr("data-placeholder")) { 193 this.options.nonSelectedText = this.$select.data("placeholder"); 194 } 195 196 // Initialization. 197 // We have to clone to create a new reference. 198 this.originalOptions = this.$select.clone()[0].options; 199 this.query = ''; 200 this.searchTimeout = null; 201 this.lastToggledInput = null; 202 203 this.options.multiple = this.$select.attr('multiple') === "multiple"; 204 this.options.onChange = $.proxy(this.options.onChange, this); 205 this.options.onSelectAll = $.proxy(this.options.onSelectAll, this); 206 this.options.onDeselectAll = $.proxy(this.options.onDeselectAll, this); 207 this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this); 208 this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this); 209 this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this); 210 this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this); 211 this.options.onInitialized = $.proxy(this.options.onInitialized, this); 212 this.options.onFiltering = $.proxy(this.options.onFiltering, this); 213 214 // Build select all if enabled. 215 this.buildContainer(); 216 this.buildButton(); 217 this.buildDropdown(); 218 this.buildReset(); 219 this.buildSelectAll(); 220 this.buildDropdownOptions(); 221 this.buildFilter(); 222 223 this.updateButtonText(); 224 this.updateSelectAll(true); 225 226 if (this.options.enableClickableOptGroups && this.options.multiple) { 227 this.updateOptGroups(); 228 } 229 230 this.options.wasDisabled = this.$select.prop('disabled'); 231 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { 232 this.disable(); 233 } 234 235 this.$select.wrap('<span class="multiselect-native-select" />').after(this.$container); 236 this.options.onInitialized(this.$select, this.$container); 237 } 238 239 Multiselect.prototype = { 240 241 defaults: { 242 /** 243 * Default text function will either print 'None selected' in case no 244 * option is selected or a list of the selected options up to a length 245 * of 3 selected options. 246 * 247 * @param {jQuery} options 248 * @param {jQuery} select 249 * @returns {String} 250 */ 251 buttonText: function(options, select) { 252 if (this.disabledText.length > 0 253 && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) { 254 255 return this.disabledText; 256 } 257 else if (options.length === 0) { 258 return this.nonSelectedText; 259 } 260 else if (this.allSelectedText 261 && options.length === $('option', $(select)).length 262 && $('option', $(select)).length !== 1 263 && this.multiple) { 264 265 if (this.selectAllNumber) { 266 return this.allSelectedText + ' (' + options.length + ')'; 267 } 268 else { 269 return this.allSelectedText; 270 } 271 } 272 else if (this.numberDisplayed != 0 && options.length > this.numberDisplayed) { 273 return options.length + ' ' + this.nSelectedText; 274 } 275 else { 276 var selected = ''; 277 var delimiter = this.delimiterText; 278 279 options.each(function() { 280 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); 281 selected += label + delimiter; 282 }); 283 284 return selected.substr(0, selected.length - this.delimiterText.length); 285 } 286 }, 287 /** 288 * Updates the aria-label of the button similar to the buttonText function. 289 * 290 * @param {jQuery} options 291 * @param {jQuery} select 292 * @returns {@exp;selected@call;substr} 293 */ 294 buttonTitle: function(options, select) { 295 if (options.length === 0) { 296 return this.nonSelectedText; 297 } 298 else { 299 var selected = ''; 300 var delimiter = this.delimiterText; 301 302 options.each(function () { 303 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); 304 selected += label + delimiter; 305 }); 306 return selected.substr(0, selected.length - this.delimiterText.length); 307 } 308 }, 309 checkboxName: function(option) { 310 return false; // no checkbox name 311 }, 312 /** 313 * Create a label. 314 * 315 * @param {jQuery} element 316 * @returns {String} 317 */ 318 optionLabel: function(element){ 319 return $(element).attr('label') || $(element).text(); 320 }, 321 /** 322 * Create a class. 323 * 324 * @param {jQuery} element 325 * @returns {String} 326 */ 327 optionClass: function(element) { 328 return $(element).attr('class') || ''; 329 }, 330 /** 331 * Triggered on change of the multiselect. 332 * 333 * Not triggered when selecting/deselecting options manually. 334 * 335 * @param {jQuery} option 336 * @param {Boolean} checked 337 */ 338 onChange : function(option, checked) { 339 340 }, 341 /** 342 * Triggered when the dropdown is shown. 343 * 344 * @param {jQuery} event 345 */ 346 onDropdownShow: function(event) { 347 348 }, 349 /** 350 * Triggered when the dropdown is hidden. 351 * 352 * @param {jQuery} event 353 */ 354 onDropdownHide: function(event) { 355 356 }, 357 /** 358 * Triggered after the dropdown is shown.
359 * 360 * @param {jQuery} event 361 */ 362 onDropdownShown: function(event) { 363 364 }, 365 /** 366 * Triggered after the dropdown is hidden. 367 * 368 * @param {jQuery} event 369 */ 370 onDropdownHidden: function(event) { 371 372 }, 373 /** 374 * Triggered on select all. 375 */ 376 onSelectAll: function() { 377 378 }, 379 /** 380 * Triggered on deselect all. 381 */ 382 onDeselectAll: function() { 383 384 }, 385 /** 386 * Triggered after initializing. 387 * 388 * @param {jQuery} $select 389 * @param {jQuery} $container 390 */ 391 onInitialized: function($select, $container) { 392 393 }, 394 /** 395 * Triggered on filtering. 396 * 397 * @param {jQuery} $filter 398 */ 399 onFiltering: function($filter) { 400 401 }, 402 enableHTML: false, 403 buttonClass: 'btn btn-default', 404 inheritClass: false, 405 buttonWidth: 'auto', 406 buttonContainer: '<div class="btn-group" role="group" aria-label="Selection" onfocusout="dropdownMenuFocusoutA11yHandler(this);" />', 407 dropRight: false, 408 dropUp: false, 409 selectedClass: 'active', 410 // Maximum height of the dropdown menu. 411 // If maximum height is exceeded a scrollbar will be displayed. 412 maxHeight: false, 413 includeSelectAllOption: false, 414 includeSelectAllIfMoreThan: 0, 415 selectAllText: ' Select all', 416 selectAllValue: 'multiselect-all', 417 selectAllName: false, 418 selectAllNumber: true, 419 selectAllJustVisible: true, 420 enableFiltering: false, 421 enableCaseInsensitiveFiltering: false, 422 enableFullValueFiltering: false, 423 enableClickableOptGroups: false, 424 enableCollapsibleOptGroups: false, 425 collapseOptGroupsByDefault: false, 426 filterPlaceholder: 'Search', 427 // possible options: 'text', 'value', 'both' 428 filterBehavior: 'text', 429 includeFilterClearBtn: true, 430 preventInputChangeEvent: false, 431 nonSelectedText: 'None selected', 432 nSelectedText: 'selected', 433 allSelectedText: 'All selected', 434 numberDisplayed: 3, 435 disableIfEmpty: false, 436 disabledText: '', 437 delimiterText: ', ', 438 includeResetOption: false, 439 includeResetDivider: false,
440 resetText: 'Reset', 441 templates: { 442 button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown" aria-expanded="false" aria-haspopup="menu"><span class="multiselect-selected-text"></span> <b class="caret"></b></button>', 443 ul: '<ul class="multiselect-container dropdown-menu" role="listbox" aria-multiselectable="true"></ul>', 444 filter: '<li class="multiselect-item multiselect-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" aria-label="Search Filter"/></div></li>', 445 filterClearBtn: '<span class="input-group-btn"><button class="btn btn-default multiselect-clear-filter" type="button" aria-label="Clear Search"><i class="glyphicon glyphicon-remove-circle"></i></button></span>', 446 li: '<li><a tabindex="0"><div><label></label></div></a></li>', 447 divider: '<li class="multiselect-item divider"></li>', 448 liGroup: '<li class="multiselect-item multiselect-group"><label></label></li>', 449 resetButton: '<li class="multiselect-reset text-center"><div class="input-group"><a class="btn btn-default btn-block"></a></div></li>' 450 } 451 }, 452 453 constructor: Multiselect, 454 455 /** 456 * Builds the container of the multiselect. 457 */ 458 buildContainer: function() { 459 this.$container = $(this.options.buttonContainer); 460 this.$container.on('show.bs.dropdown', this.options.onDropdownShow); 461 this.$container.on('hide.bs.dropdown', this.options.onDropdownHide); 462 this.$container.on('shown.bs.dropdown', this.options.onDropdownShown); 463 this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden); 464 }, 465 466 /** 467 * Builds the button of the multiselect. 468 */ 469 buildButton: function() { 470 this.$button = $(this.options.templates.button).addClass(this.options.buttonClass); 471 if (this.$select.attr('class') && this.options.inheritClass) { 472 this.$button.addClass(this.$select.attr('class')); 473 } 474 // Adopt active state. 475 if (this.$select.prop('disabled')) { 476 this.disable(); 477 } 478 else { 479 this.enable(); 480 } 481 482 // Manually add button width if set. 483 if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') { 484 this.$button.css({ 485 'width' : '100%', //this.options.buttonWidth, 486 'overflow' : 'hidden', 487 'text-overflow' : 'ellipsis' 488 }); 489 this.$container.css({ 490 'width': this.options.buttonWidth 491 }); 492 } 493 494 // Keep the tab index from the select. 495 var tabindex = this.$select.attr('tabindex'); 496 if (tabindex) { 497 this.$button.attr('tabindex', tabindex); 498 } 499 500 this.$container.prepend(this.$button); 501 }, 502 503 /** 504 * Builds the ul representing the dropdown menu. 505 */ 506 buildDropdown: function() { 507 508 // Build ul. 509 this.$ul = $(this.options.templates.ul); 510 511 if (this.options.dropRight) { 512 this.$ul.addClass('pull-right'); 513 } 514 515 // Set max height of dropdown menu to activate auto scrollbar. 516 if (this.options.maxHeight) { 517 // TODO: Add a class for this option to move the css declarations. 518 this.$ul.css({ 519 'max-height': this.options.maxHeight + 'px', 520 'overflow-y': 'auto', 521 'overflow-x': 'hidden' 522 }); 523 } 524 525 if (this.options.dropUp) { 526 527 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)); 528 var moveCalc = height + 34; 529 530 this.$ul.css({ 531 'max-height': height + 'px', 532 'overflow-y': 'auto', 533 'overflow-x': 'hidden', 534 'margin-top': "-" + moveCalc + 'px' 535 }); 536 } 537 538 this.$container.append(this.$ul); 539 }, 540 541 /** 542 * Build the dropdown options and binds all necessary events. 543 * 544 * Uses createDivider and createOptionValue to create the necessary options. 545 */ 546 buildDropdownOptions: function() { 547 548 this.$select.children().each($.proxy(function(index, element) { 549 550 var $element = $(element);
vendor: 4,846 bytes, lines 551-665
551 // Support optgroups and options without a group simultaneously. 552 var tag = $element.prop('tagName') 553 .toLowerCase(); 554 555 if ($element.prop('value') === this.options.selectAllValue) { 556 return; 557 } 558 559 if (tag === 'optgroup') { 560 this.createOptgroup(element); 561 } 562 else if (tag === 'option') { 563 564 if ($element.data('role') === 'divider') { 565 this.createDivider(); 566 } 567 else { 568 this.createOptionValue(element); 569 } 570 571 } 572 573 // Other illegal tags will be ignored. 574 }, this)); 575 576 // Bind the change event on the dropdown elements. 577 $(this.$ul).off('change', 'li:not(.multiselect-group) input[type="checkbox"], li:not(.multiselect-group) input[type="radio"]'); 578 $(this.$ul).on('change', 'li:not(.multiselect-group) input[type="checkbox"], li:not(.multiselect-group) input[type="radio"]', $.proxy(function(event) { 579 var $target = $(event.target); 580 581 var checked = $target.prop('checked') || false; 582 var isSelectAllOption = $target.val() === this.options.selectAllValue; 583 584 // Apply or unapply the configured selected class. 585 if (this.options.selectedClass) { 586 if (checked) { 587 $target.closest('li') 588 .addClass(this.options.selectedClass); 589 } 590 else { 591 $target.closest('li') 592 .removeClass(this.options.selectedClass); 593 } 594 } 595 596 // Get the corresponding option. 597 var value = $target.val(); 598 var $option = this.getOptionByValue(value); 599 600 // Handle radio buttons 601 if ($target.is('input[type="radio"]')) { 602 // Just handle the visual state and skip all select-related logic 603 if (this.options.selectedClass) { 604 if (checked) { 605 $target.closest('li').addClass(this.options.selectedClass); 606 } else { 607 $target.closest('li').removeClass(this.options.selectedClass); 608 } 609 } 610 this.updateButtonText(); 611 612 // When it's not a single selection: exit early - no dropdown closing, no select option manipulation 613 if(this.options.multiple) { 614 if(this.options.preventInputChangeEvent) { 615 return false; 616 } 617 return; 618 } 619 } 620 621 var $optionsNotThis = $('option', this.$select).not($option); 622 var $checkboxesNotThis = $('input', this.$container).not($target); 623 624 if (isSelectAllOption) { 625 626 if (checked) { 627 this.selectAll(this.options.selectAllJustVisible, true); 628 } 629 else { 630 this.deselectAll(this.options.selectAllJustVisible, true); 631 } 632 } 633 else { 634 if (checked) { 635 $option.prop('selected', true); 636 637 if (this.options.multiple) { 638 // Simply select additional option. 639 $option.prop('selected', true); 640 } 641 else { 642 // Unselect all other options and corresponding checkboxes. 643 if (this.options.selectedClass) { 644 $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass); 645 } 646 647 $($checkboxesNotThis).prop('checked', false); 648 $optionsNotThis.prop('selected', false); 649 650 // It's a single selection, so close. 651 this.$button.click(); 652 } 653 654 if (this.options.selectedClass === "active") { 655 $optionsNotThis.closest("a").css("outline", ""); 656 } 657 } 658 else { 659 // Unselect option. 660 $option.prop('selected', false); 661 } 662 663 // To prevent select all from firing onChange: #575 664 this.options.onChange($option, checked); 665
666 // Do not update select all or optgroups on select all change! 667 this.updateSelectAll(); 668 669 if (this.options.enableClickableOptGroups && this.options.multiple) { 670 this.updateOptGroups(); 671 } 672 } 673 674 this.$select.change(); 675 this.updateButtonText(); 676 677 if(this.options.preventInputChangeEvent) { 678 return false; 679 } 680 }, this)); 681 682 $('li a', this.$ul).on('mousedown', function(e) { 683 if (e.shiftKey) { 684 // Prevent selecting text by Shift+click 685 return false; 686 } 687 }); 688 689 $(this.$ul).on('touchstart click', 'li a', $.proxy(function(event) { 690 event.stopPropagation(); 691 692 var $target = $(event.target); 693 694 if (event.shiftKey && this.options.multiple) { 695 if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) 696 event.preventDefault(); 697 $target = $target.find("input"); 698 $target.prop("checked", !$target.prop("checked")); 699 } 700 var checked = $target.prop('checked') || false; 701 702 if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range 703 var from = this.$ul.find("li:visible").index($target.parents("li")); 704 var to = this.$ul.find("li:visible").index(this.lastToggledInput.parents("li")); 705 706 if (from > to) { // Swap the indices 707 var tmp = to; 708 to = from; 709 from = tmp; 710 } 711 712 // Make sure we grab all elements since slice excludes the last index 713 ++to; 714 715 // Change the checkboxes and underlying options 716 var range = this.$ul.find("li").not(".multiselect-filter-hidden").slice(from, to).find("input"); 717 718 range.prop('checked', checked); 719 720 if (this.options.selectedClass) { 721 range.closest('li') 722 .toggleClass(this.options.selectedClass, checked); 723 } 724 725 for (var i = 0, j = range.length; i < j; i++) { 726 var $checkbox = $(range[i]); 727 728 var $option = this.getOptionByValue($checkbox.val()); 729 730 $option.prop('selected', checked); 731 } 732 } 733 734 // Trigger the select "change" event 735 $target.trigger("change"); 736 } 737 738 // Remembers last clicked option 739 if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ 740 this.lastToggledInput = $target; 741 } 742 743 $target.blur(); 744 }, this)); 745 746 // Keyboard support. 747 this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { 748 if ($('input[type="text"]', this.$container).is(':focus')) { 749 return; 750 } 751 752 var $items = $(this.$container).find("li[role='option']:not(.disabled), input[type='radio']:not(.disabled)").filter(":visible"); 753 754 if (!$items.length) { 755 return; 756 } 757 758 // Tab key navigation - focus on li elements 759 if (event.keyCode === 9) { 760 var currentFocused = $items.filter(':focus'); 761 var currentIndex = $items.index(currentFocused); 762 763 if (event.shiftKey) { 764 // Shift+Tab - go to previous item or exit 765 if (currentIndex > 0) { 766 $items.eq(currentIndex - 1).focus(); 767 event.preventDefault(); 768 } else {
769 // At first item or no focus, close the dropdown if it is one and do nothing to let tab continue naturally 770 if (this.$ul.hasClass('dropdown-menu')) { 771 this.$button.click(); 772 } 773 } 774 } else { 775 // Tab - go to next item or exit 776 if (currentIndex >= 0 && currentIndex < $items.length - 1) { 777 $items.eq(currentIndex + 1).focus(); 778 event.preventDefault(); 779 } else if (currentIndex === -1) { 780 // First tab into dropdown 781 $items.eq(0).focus(); 782 event.preventDefault(); 783 } else { 784 // At last item, close the dropdown if it is one and do nothing to let tab continue naturally 785 if (this.$ul.hasClass('dropdown-menu')) { 786 this.$button.click(); 787 } else { 788 // Since there is no dropdowns to close, the focus will go to the link inside the li which has a tabindex=0. 789 // The focus will not be visible and the next time the user types the tab key, the currentIndex will be -1 790 // and it will focus again the options from the start. 791 // If there is no dropdowns to close, we need to find the second next focusable element in the dom, 792 // skipping the link contained in the li. 793 var focusableSelectors = 'li[role="option"]:not(.disabled), input[type="radio"]:not(.disabled), a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; 794 var $allFocusable = $(focusableSelectors).filter(':visible').not('[aria-hidden="true"]'); 795 var containerIndex = $allFocusable.index(currentFocused); 796 if (containerIndex >= 0 && containerIndex < $allFocusable.length - 2) { 797 $allFocusable.eq(containerIndex + 2).focus(); // +2 to skip the next one which is a link in the li. 798 event.preventDefault(); 799 } 800 } 801 } 802 } 803 } 804 // Arrow key navigation 805 else { 806 var index = $items.index($items.filter(':focus')); 807 var newIndex = index; 808 809 // Navigation up 810 if (event.keyCode === 38) { 811 if (index > 0) { 812 newIndex = index - 1; 813 } else if (index === 0) { 814 // At first item, exit dropdown 815 this.$button.focus(); 816 this.$button.click(); 817 return; 818 } else if (index === -1) { 819 // No item focused, focus last item 820 newIndex = $items.length - 1; 821 } 822 } 823 // Navigate down 824 else if (event.keyCode === 40) { 825 if (index >= 0 && index < $items.length - 1) { 826 newIndex = index + 1; 827 } else if (index === $items.length - 1) { 828 // At last item, exit dropdown 829 this.$button.focus(); 830 this.$button.click(); 831 return; 832 } else if (index === -1) { 833 // No item focused, focus first item 834 newIndex = 0; 835 } 836 } 837 838 // Only proceed if we have a valid index 839 if (newIndex >= 0 && newIndex < $items.length) { 840 var $current = $items.eq(newIndex); 841 $current.focus(); 842 } 843 844 if (event.keyCode === 32 || event.keyCode === 13) {
845 var $focusedItem = $items.filter(':focus'); 846 if ($focusedItem.length) { 847 // Handle radio buttons 848 if ($focusedItem.is('input[type="radio"]')) { 849 // For radio buttons, handle the state change manually without triggering change event 850 // First, uncheck other radio buttons in the same group 851 var radioName = $focusedItem.attr('name'); 852 if (radioName) { 853 $('input[type="radio"][name="' + radioName + '"]', this.$container).prop("checked", false); 854 } 855 // Check the focused radio button 856 $focusedItem.prop("checked", true); 857 // Update visual state manually (without triggering change event) 858 if (this.options.selectedClass) { 859 $('input[type="radio"][name="' + radioName + '"]', this.$container).closest('li').removeClass(this.options.selectedClass); 860 $focusedItem.closest('li').addClass(this.options.selectedClass); 861 } 862 863 this.updateButtonText(); 864 865 // Trigger a custom radioclick event on the .radio container div 866 // This will be caught by the new radioclick handler in listing_bootstrap__.inc 867 // which doesn't have the problematic button click that closes the dropdown 868 setTimeout(function() { 869 $focusedItem.closest('.radio').trigger('radioclick'); 870 }, 0); 871 872 // Prevent any further event processing 873 event.stopPropagation(); 874 event.preventDefault(); 875 return false; 876 } else { 877 // Handle regular checkboxes 878 var $checkbox = $focusedItem.find('input[aria-hidden="true"]'); 879 var isChecked = !$checkbox.prop("checked"); 880 881 $checkbox.prop("checked", isChecked); 882 $focusedItem.attr('aria-selected', isChecked ? 'true' : 'false'); 883 $checkbox.change(); 884 } 885 } 886 } 887 888 event.stopPropagation(); 889 event.preventDefault(); 890 } 891 }, this)); 892 893 if (this.options.enableClickableOptGroups && this.options.multiple) { 894 $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { 895 event.stopPropagation(); 896 897 var $target = $(event.target); 898 var checked = $target.prop('checked') || false; 899 900 var $li = $(event.target).closest('li'); 901 var $group = $li.nextUntil("li.multiselect-group") 902 .not('.multiselect-filter-hidden') 903 .not('.disabled'); 904 905 var $inputs = $group.find("input"); 906 907 var values = []; 908 var $options = []; 909 910 if (this.options.selectedClass) { 911 if (checked) { 912 $li.addClass(this.options.selectedClass); 913 } 914 else { 915 $li.removeClass(this.options.selectedClass); 916 } 917 } 918 919 $.each($inputs, $.proxy(function(index, input) { 920 var value = $(input).val(); 921 var $option = this.getOptionByValue(value); 922 923 if (checked) { 924 $(input).prop('checked', true); 925 $(input).closest('li') 926 .addClass(this.options.selectedClass); 927 928 $option.prop('selected', true); 929 } 930 else { 931 $(input).prop('checked', false); 932 $(input).closest('li') 933 .removeClass(this.options.selectedClass); 934 935 $option.prop('selected', false); 936 } 937 938 $options.push(this.getOptionByValue(value)); 939 }, this)) 940 941 // Cannot use select or deselect here because it would call updateOptGroups again. 942 943 this.options.onChange($options, checked); 944 945 this.$select.change(); 946 this.updateButtonText(); 947 this.updateSelectAll(); 948 }, this)); 949 } 950 951 if (this.options.enableCollapsibleOptGroups && this.options.multiple) { 952 $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { 953 var $li = $(event.target).closest('li');
954 var $inputs = $li.nextUntil("li.multiselect-group") 955 .not('.multiselect-filter-hidden'); 956 957 var visible = true; 958 $inputs.each(function() { 959 visible = visible && !$(this).hasClass('multiselect-collapsible-hidden'); 960 }); 961 962 if (visible) { 963 $inputs.hide() 964 .addClass('multiselect-collapsible-hidden'); 965 } 966 else { 967 $inputs.show() 968 .removeClass('multiselect-collapsible-hidden'); 969 } 970 }, this)); 971 972 $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); 973 $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); 974 $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); 975 } 976 }, 977 978 /** 979 * Create an option using the given select option. 980 * 981 * @param {jQuery} element 982 */ 983 createOptionValue: function(element) { 984 var $element = $(element); 985 if ($element.is(':selected')) { 986 $element.prop('selected', true); 987 } 988 989 // Support the label attribute on options. 990 var label = this.options.optionLabel(element); 991 var classes = this.options.optionClass(element); 992 var value = $element.val(); 993 var inputType = this.options.multiple ? "checkbox" : "radio"; 994 995 var $li = $(this.options.templates.li); 996 var $label = $('label', $li); 997 var $div = $('div', $li); 998 $div.addClass(inputType); 999 $label.attr("aria-label", label);
1000 $li.addClass(classes); 1001 1002 // Hide all children items when collapseOptGroupsByDefault is true 1003 if (this.options.collapseOptGroupsByDefault && $(element).parent().prop("tagName").toLowerCase() === "optgroup") { 1004 $li.addClass("multiselect-collapsible-hidden"); 1005 $li.hide(); 1006 } 1007 1008 if (this.options.enableHTML) { 1009 $label.html(label); 1010 } 1011 else { 1012 $label.text(" " + label); 1013 } 1014 1015 var $checkbox = $('<input/>').attr({ 1016 'type': inputType, 1017 'aria-hidden': 'true' // Hide from screen readers 1018 }); 1019 1020 var name = this.options.checkboxName($element); 1021 if (name) { 1022 $checkbox.attr('name', name); 1023 } 1024 1025 $div.prepend($checkbox); 1026 1027 var selected = $element.prop('selected') || false; 1028 $checkbox.val(value); 1029 1030 $checkbox.attr('id', this.$select.attr('name') + '__' + inputType + '_' + value); 1031 $label.attr('for', this.$select.attr('name') + '__' + inputType + '_' + value); 1032 1033 // Add accessibility attributes to the li 1034 $li.attr({ 1035 'role': 'option', 1036 'aria-selected': selected ? 'true' : 'false', 1037 'data-value': value, 1038 'tabindex': '0' // Make li elements focusable 1039 }); 1040 1041 if (value === this.options.selectAllValue) { 1042 $li.addClass("multiselect-item multiselect-all"); 1043 $checkbox.parent().parent() 1044 .addClass('multiselect-all'); 1045 } else { 1046 $li.addClass("acc-keyboard-mode--multiselect-item"); 1047 } 1048 1049 $label.attr('aria-label', $element.attr('aria-label')); 1050 1051 this.$ul.append($li); 1052 1053 if ($element.is(':disabled')) { 1054 $checkbox.attr('disabled', 'disabled').prop('disabled', true); 1055 $li.attr('tabindex', '-1') 1056 .attr('aria-disabled', 'true') 1057 .addClass('disabled'); 1058 } 1059 1060 $checkbox.prop('checked', selected); 1061 1062 if (selected && this.options.selectedClass) { 1063 $li.addClass(this.options.selectedClass); 1064 } 1065 }, 1066 1067 /** 1068 * Creates a divider using the given select option. 1069 * 1070 * @param {jQuery} element 1071 */ 1072 createDivider: function(element) { 1073 var $divider = $(this.options.templates.divider); 1074 this.$ul.append($divider); 1075 }, 1076 1077 /** 1078 * Creates an optgroup. 1079 * 1080 * @param {jQuery} group 1081 */ 1082 createOptgroup: function(group) { 1083 var label = $(group).attr("label"); 1084 var value = $(group).attr("value"); 1085 var $li = $('<li class="multiselect-item multiselect-group"><a href="javascript:void(0);"><label><b></b></label></a></li>'); 1086 1087 var classes = this.options.optionClass(group);
1088 $li.addClass(classes); 1089 1090 if (this.options.enableHTML) { 1091 $('label b', $li).html(" " + label); 1092 } 1093 else { 1094 $('label b', $li).text(" " + label); 1095 } 1096 1097 if (this.options.enableCollapsibleOptGroups && this.options.multiple) { 1098 $('a', $li).append('<span class="caret-container"><b class="caret"></b></span>'); 1099 } 1100 1101 if (this.options.enableClickableOptGroups && this.options.multiple) { 1102 $('label', $li).prepend('<input type="checkbox" value="' + value + '"/>'); 1103 } 1104 1105 if ($(group).is(':disabled')) { 1106 $li.addClass('disabled'); 1107 } 1108 1109 this.$ul.append($li); 1110 1111 $("option", group).each($.proxy(function($, group) { 1112 this.createOptionValue(group); 1113 }, this)) 1114 }, 1115 1116 /** 1117 * Build the reset. 1118 * 1119 */ 1120 buildReset: function() { 1121 if (this.options.includeResetOption) { 1122 1123 // Check whether to add a divider after the reset. 1124 if (this.options.includeResetDivider) { 1125 this.$ul.prepend($(this.options.templates.divider)); 1126 } 1127 1128 var $resetButton = $(this.options.templates.resetButton); 1129 1130 if (this.options.enableHTML) { 1131 $('a', $resetButton).html(this.options.resetText); 1132 } 1133 else { 1134 $('a', $resetButton).text(this.options.resetText); 1135 } 1136 1137 $('a', $resetButton).click($.proxy(function(){ 1138 this.clearSelection(); 1139 }, this)); 1140 1141 this.$ul.prepend($resetButton); 1142 } 1143 }, 1144 1145 /** 1146 * Build the select all. 1147 * 1148 * Checks if a select all has already been created. 1149 */ 1150 buildSelectAll: function() { 1151 if (typeof this.options.selectAllValue === 'number') { 1152 this.options.selectAllValue = this.options.selectAllValue.toString(); 1153 } 1154 1155 var alreadyHasSelectAll = this.hasSelectAll(); 1156 1157 if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple 1158 && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) { 1159 1160 // Check whether to add a divider after the select all. 1161 if (this.options.includeSelectAllDivider) { 1162 this.$ul.prepend($(this.options.templates.divider)); 1163 } 1164 1165 var $li = $(this.options.templates.li); 1166 $('div', $li).addClass("checkbox"); 1167 1168 if (this.options.enableHTML) { 1169 $('label', $li).html(" " + this.options.selectAllText); 1170 } 1171 else { 1172 $('label', $li).text(" " + this.options.selectAllText); 1173 } 1174 1175 if (this.options.selectAllName) { 1176 $('div', $li).prepend('<input type="checkbox" name="' + this.options.selectAllName + '" />'); 1177 } 1178 else { 1179 $('div', $li).prepend('<input type="checkbox" />'); 1180 } 1181 1182 var $checkbox = $('input', $li); 1183 $checkbox.val(this.options.selectAllValue); 1184 1185 $li.addClass("multiselect-item multiselect-all"); 1186 $checkbox.parent().parent() 1187 .addClass('multiselect-all'); 1188 1189 this.$ul.prepend($li); 1190 1191 $checkbox.prop('checked', false); 1192 } 1193 }, 1194 1195 /** 1196 * Builds the filter. 1197 */ 1198 buildFilter: function() { 1199 1200 // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength. 1201 if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) { 1202 var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering); 1203 1204 if (this.$select.find('option').length >= enableFilterLength) { 1205 1206 this.$filter = $(this.options.templates.filter); 1207 $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder); 1208 1209 // Adds optional filter clear button 1210 if(this.options.includeFilterClearBtn) { 1211 var clearBtn = $(this.options.templates.filterClearBtn); 1212 var clearAction = $.proxy(function(event){ 1213 clearTimeout(this.searchTimeout); 1214 1215 this.query = ''; 1216 this.$filter.find('.multiselect-search').val(''); 1217 $('li', this.$ul).show().removeClass('multiselect-filter-hidden'); 1218 1219 this.updateSelectAll(); 1220 1221 if (this.options.enableClickableOptGroups && this.options.multiple) { 1222 this.updateOptGroups(); 1223 } 1224 1225 }, this); 1226 1227 clearBtn.on('click', clearAction); 1228 1229 // Add keyboard support for Enter and Space keys 1230 clearBtn.on('keyup', $.proxy(function(event){ 1231 // Enter (keyCode 13) or Space (keyCode 32) 1232 if (event.keyCode === 13 || event.keyCode === 32) { 1233 event.preventDefault(); 1234 event.stopPropagation(); 1235 clearAction(event); 1236 // Return focus to search input after clearing 1237 this.$filter.find('.multiselect-search').focus(); 1238 } 1239 }, this)); 1240 1241 this.$filter.find('.input-group').append(clearBtn); 1242 } 1243 1244 this.$ul.prepend(this.$filter); 1245 1246 this.$filter.val(this.query).on('click', function(event) { 1247 event.stopPropagation(); 1248 }).on('input keydown', $.proxy(function(event) { 1249 // Only process events originating from the search input; 1250 // ignore bubbled keydowns from the clear-filter button 1251 // (whose empty .value would reset the filter query). 1252 if (!$(event.target).hasClass('multiselect-search')) { 1253 return; 1254 } 1255 1256 // Cancel enter key default behaviour 1257 if (event.which === 13) { 1258 event.preventDefault(); 1259 } 1260 1261 // This is useful to catch "keydown" events after the browser has updated the control. 1262 clearTimeout(this.searchTimeout); 1263 1264 this.searchTimeout = this.asyncFunction($.proxy(function() { 1265 1266 if (this.query !== event.target.value) { 1267 this.query = event.target.value; 1268 1269 var currentGroup, currentGroupVisible; 1270 $.each($('li', this.$ul), $.proxy(function(index, element) { 1271 var value = $('input', element).length > 0 ? $('input', element).val() : ""; 1272 var text = $('label', element).text(); 1273 1274 var filterCandidate = ''; 1275 if ((this.options.filterBehavior === 'text')) { 1276 filterCandidate = text; 1277 } 1278 else if ((this.options.filterBehavior === 'value')) { 1279 filterCandidate = value; 1280 } 1281 else if (this.options.filterBehavior === 'both') { 1282 filterCandidate = text + '\n' + value; 1283 } 1284 1285 if (value !== this.options.selectAllValue && text) { 1286 1287 // By default lets assume that element is not 1288 // interesting for this search. 1289 var showElement = false;
vendor: 20,177 bytes, lines 1290-1825
1290 1291 if (this.options.enableCaseInsensitiveFiltering) { 1292 filterCandidate = filterCandidate.toLowerCase(); 1293 this.query = this.query.toLowerCase(); 1294 } 1295 1296 if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') { 1297 var valueToMatch = filterCandidate.trim().substring(0, this.query.length); 1298 if (this.query.indexOf(valueToMatch) > -1) { 1299 showElement = true; 1300 } 1301 } 1302 else if (filterCandidate.indexOf(this.query) > -1) { 1303 showElement = true; 1304 } 1305 1306 // Toggle current element (group or group item) according to showElement boolean. 1307 if(!showElement){ 1308 $(element).css('display', 'none'); 1309 $(element).addClass('multiselect-filter-hidden'); 1310 } 1311 if(showElement){ 1312 $(element).css('display', 'block'); 1313 $(element).removeClass('multiselect-filter-hidden'); 1314 } 1315 1316 // Differentiate groups and group items. 1317 if ($(element).hasClass('multiselect-group')) { 1318 // Remember group status. 1319 currentGroup = element; 1320 currentGroupVisible = showElement; 1321 } 1322 else { 1323 // Show group name when at least one of its items is visible. 1324 if (showElement) { 1325 $(currentGroup).show() 1326 .removeClass('multiselect-filter-hidden'); 1327 } 1328 1329 // Show all group items when group name satisfies filter. 1330 if (!showElement && currentGroupVisible) { 1331 $(element).show() 1332 .removeClass('multiselect-filter-hidden'); 1333 } 1334 } 1335 } 1336 }, this)); 1337 } 1338 1339 this.updateSelectAll(); 1340 1341 if (this.options.enableClickableOptGroups && this.options.multiple) { 1342 this.updateOptGroups(); 1343 } 1344 1345 this.options.onFiltering(event.target); 1346 1347 }, this), 300, this); 1348 }, this)); 1349 } 1350 } 1351 }, 1352 1353 /** 1354 * Unbinds the whole plugin. 1355 */ 1356 destroy: function() { 1357 this.$container.remove(); 1358 this.$select.show(); 1359 1360 // reset original state 1361 this.$select.prop('disabled', this.options.wasDisabled); 1362 1363 this.$select.data('multiselect', null); 1364 }, 1365 1366 /** 1367 * Refreshs the multiselect based on the selected options of the select. 1368 */ 1369 refresh: function () { 1370 var inputs = {}; 1371 $('li input', this.$ul).each(function() { 1372 inputs[$(this).val()] = $(this); 1373 }); 1374 1375 $('option', this.$select).each($.proxy(function (index, element) { 1376 var $elem = $(element); 1377 var $input = inputs[$(element).val()]; 1378 1379 if ($elem.is(':selected')) { 1380 $input.prop('checked', true); 1381 1382 if (this.options.selectedClass) { 1383 $input.closest('li') 1384 .addClass(this.options.selectedClass); 1385 } 1386 } 1387 else { 1388 $input.prop('checked', false); 1389 1390 if (this.options.selectedClass) { 1391 $input.closest('li') 1392 .removeClass(this.options.selectedClass); 1393 } 1394 } 1395 1396 if ($elem.is(":disabled")) { 1397 $input.attr('disabled', 'disabled') 1398 .prop('disabled', true) 1399 .closest('li') 1400 .addClass('disabled'); 1401 } 1402 else { 1403 $input.prop('disabled', false) 1404 .closest('li') 1405 .removeClass('disabled'); 1406 } 1407 }, this)); 1408 1409 this.updateButtonText(); 1410 this.updateSelectAll(); 1411 1412 if (this.options.enableClickableOptGroups && this.options.multiple) { 1413 this.updateOptGroups(); 1414 } 1415 }, 1416 1417 /** 1418 * Select all options of the given values. 1419 * 1420 * If triggerOnChange is set to true, the on change event is triggered if 1421 * and only if one value is passed. 1422 * 1423 * @param {Array} selectValues 1424 * @param {Boolean} triggerOnChange 1425 */ 1426 select: function(selectValues, triggerOnChange) { 1427 if(!$.isArray(selectValues)) { 1428 selectValues = [selectValues]; 1429 } 1430 1431 for (var i = 0; i < selectValues.length; i++) { 1432 var value = selectValues[i]; 1433 1434 if (value === null || value === undefined) { 1435 continue; 1436 } 1437 1438 var $option = this.getOptionByValue(value); 1439 var $checkbox = this.getInputByValue(value); 1440 1441 if($option === undefined || $checkbox === undefined) { 1442 continue; 1443 } 1444 1445 if (!this.options.multiple) { 1446 this.deselectAll(false); 1447 } 1448 1449 if (this.options.selectedClass) { 1450 $checkbox.closest('li') 1451 .addClass(this.options.selectedClass); 1452 } 1453 1454 $checkbox.prop('checked', true); 1455 $option.prop('selected', true); 1456 1457 if (triggerOnChange) { 1458 this.options.onChange($option, true); 1459 } 1460 } 1461 1462 this.updateButtonText(); 1463 this.updateSelectAll(); 1464 1465 if (this.options.enableClickableOptGroups && this.options.multiple) { 1466 this.updateOptGroups(); 1467 } 1468 }, 1469 1470 /** 1471 * Clears all selected items. 1472 */ 1473 clearSelection: function () { 1474 this.deselectAll(false); 1475 this.updateButtonText(); 1476 this.updateSelectAll(); 1477 1478 if (this.options.enableClickableOptGroups && this.options.multiple) { 1479 this.updateOptGroups(); 1480 } 1481 }, 1482 1483 /** 1484 * Deselects all options of the given values. 1485 * 1486 * If triggerOnChange is set to true, the on change event is triggered, if 1487 * and only if one value is passed. 1488 * 1489 * @param {Array} deselectValues 1490 * @param {Boolean} triggerOnChange 1491 */ 1492 deselect: function(deselectValues, triggerOnChange) { 1493 if(!$.isArray(deselectValues)) { 1494 deselectValues = [deselectValues]; 1495 } 1496 1497 for (var i = 0; i < deselectValues.length; i++) { 1498 var value = deselectValues[i]; 1499 1500 if (value === null || value === undefined) { 1501 continue; 1502 } 1503 1504 var $option = this.getOptionByValue(value); 1505 var $checkbox = this.getInputByValue(value); 1506 1507 if($option === undefined || $checkbox === undefined) { 1508 continue; 1509 } 1510 1511 if (this.options.selectedClass) { 1512 $checkbox.closest('li') 1513 .removeClass(this.options.selectedClass); 1514 } 1515 1516 $checkbox.prop('checked', false); 1517 $option.prop('selected', false); 1518 1519 if (triggerOnChange) { 1520 this.options.onChange($option, false); 1521 } 1522 } 1523 1524 this.updateButtonText(); 1525 this.updateSelectAll(); 1526 1527 if (this.options.enableClickableOptGroups && this.options.multiple) { 1528 this.updateOptGroups(); 1529 } 1530 }, 1531 1532 /** 1533 * Selects all enabled & visible options. 1534 * 1535 * If justVisible is true or not specified, only visible options are selected. 1536 * 1537 * @param {Boolean} justVisible 1538 * @param {Boolean} triggerOnSelectAll 1539 */ 1540 selectAll: function (justVisible, triggerOnSelectAll) { 1541 1542 var justVisible = typeof justVisible === 'undefined' ? true : justVisible; 1543 var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); 1544 var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); 1545 1546 if(justVisible) { 1547 $('input:enabled' , visibleLis).prop('checked', true); 1548 visibleLis.addClass(this.options.selectedClass); 1549 1550 $('input:enabled' , visibleLis).each($.proxy(function(index, element) { 1551 var value = $(element).val(); 1552 var option = this.getOptionByValue(value); 1553 $(option).prop('selected', true); 1554 }, this)); 1555 } 1556 else { 1557 $('input:enabled' , allLis).prop('checked', true); 1558 allLis.addClass(this.options.selectedClass); 1559 1560 $('input:enabled' , allLis).each($.proxy(function(index, element) { 1561 var value = $(element).val(); 1562 var option = this.getOptionByValue(value); 1563 $(option).prop('selected', true); 1564 }, this)); 1565 } 1566 1567 $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true); 1568 1569 if (this.options.enableClickableOptGroups && this.options.multiple) { 1570 this.updateOptGroups(); 1571 } 1572 1573 if (triggerOnSelectAll) { 1574 this.options.onSelectAll(); 1575 } 1576 }, 1577 1578 /** 1579 * Deselects all options. 1580 * 1581 * If justVisible is true or not specified, only visible options are deselected. 1582 * 1583 * @param {Boolean} justVisible 1584 */ 1585 deselectAll: function (justVisible, triggerOnDeselectAll) { 1586 1587 var justVisible = typeof justVisible === 'undefined' ? true : justVisible; 1588 var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); 1589 var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); 1590 1591 if(justVisible) { 1592 $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false); 1593 visibleLis.removeClass(this.options.selectedClass); 1594 1595 $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) { 1596 var value = $(element).val(); 1597 var option = this.getOptionByValue(value); 1598 $(option).prop('selected', false); 1599 }, this)); 1600 } 1601 else { 1602 $('input[type="checkbox"]:enabled' , allLis).prop('checked', false); 1603 allLis.removeClass(this.options.selectedClass); 1604 1605 $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) { 1606 var value = $(element).val(); 1607 var option = this.getOptionByValue(value); 1608 $(option).prop('selected', false); 1609 }, this)); 1610 } 1611 1612 $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false); 1613 1614 if (this.options.enableClickableOptGroups && this.options.multiple) { 1615 this.updateOptGroups(); 1616 } 1617 1618 if (triggerOnDeselectAll) { 1619 this.options.onDeselectAll(); 1620 } 1621 }, 1622 1623 /** 1624 * Rebuild the plugin. 1625 * 1626 * Rebuilds the dropdown, the filter and the select all option. 1627 */ 1628 rebuild: function() { 1629 this.$ul.html(''); 1630 1631 // Important to distinguish between radios and checkboxes. 1632 this.options.multiple = this.$select.attr('multiple') === "multiple"; 1633 1634 this.buildSelectAll(); 1635 this.buildDropdownOptions(); 1636 this.buildFilter(); 1637 1638 this.updateButtonText(); 1639 this.updateSelectAll(true); 1640 1641 if (this.options.enableClickableOptGroups && this.options.multiple) { 1642 this.updateOptGroups(); 1643 } 1644 1645 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { 1646 this.disable(); 1647 } 1648 else { 1649 this.enable(); 1650 } 1651 1652 if (this.options.dropRight) { 1653 this.$ul.addClass('pull-right'); 1654 } 1655 }, 1656 1657 /** 1658 * The provided data will be used to build the dropdown. 1659 */ 1660 dataprovider: function(dataprovider) { 1661 1662 var groupCounter = 0; 1663 var $select = this.$select.empty(); 1664 1665 $.each(dataprovider, function (index, option) { 1666 var $tag; 1667 1668 if ($.isArray(option.children)) { // create optiongroup tag 1669 groupCounter++; 1670 1671 $tag = $('<optgroup/>').attr({ 1672 label: option.label || 'Group ' + groupCounter, 1673 disabled: !!option.disabled, 1674 value: option.value 1675 }); 1676 1677 forEach(option.children, function(subOption) { // add children option tags 1678 var attributes = { 1679 value: subOption.value, 1680 label: subOption.label || subOption.value, 1681 ariaLabel: subOption.ariaLabel, 1682 selected: !!subOption.selected, 1683 disabled: !!subOption.disabled 1684 }; 1685 1686 //Loop through attributes object and add key-value for each attribute 1687 for (var key in subOption.attributes) { 1688 attributes['data-' + key] = subOption.attributes[key]; 1689 } 1690 //Append original attributes + new data attributes to option 1691 $tag.append($('<option/>').attr(attributes)); 1692 }); 1693 } 1694 else { 1695 1696 var attributes = { 1697 'value': option.value, 1698 'label': option.label || option.value, 1699 'ariaLabel': option.ariaLabel, 1700 'class': option['class'], 1701 'selected': !!option['selected'], 1702 'disabled': !!option['disabled'] 1703 }; 1704 //Loop through attributes object and add key-value for each attribute 1705 for (var key in option.attributes) { 1706 attributes['data-' + key] = option.attributes[key]; 1707 } 1708 //Append original attributes + new data attributes to option 1709 $tag = $('<option/>').attr(attributes); 1710 1711 $tag.text(option.label || option.value); 1712 } 1713 1714 $select.append($tag); 1715 }); 1716 1717 this.rebuild(); 1718 }, 1719 1720 /** 1721 * Enable the multiselect. 1722 */ 1723 enable: function() { 1724 this.$select.prop('disabled', false); 1725 this.$button.prop('disabled', false) 1726 .removeClass('disabled'); 1727 }, 1728 1729 /** 1730 * Disable the multiselect. 1731 */ 1732 disable: function() { 1733 this.$select.prop('disabled', true); 1734 this.$button.prop('disabled', true) 1735 .addClass('disabled'); 1736 }, 1737 1738 /** 1739 * Set the options. 1740 * 1741 * @param {Array} options 1742 */ 1743 setOptions: function(options) { 1744 this.options = this.mergeOptions(options); 1745 }, 1746 1747 /** 1748 * Merges the given options with the default options. 1749 * 1750 * @param {Array} options 1751 * @returns {Array} 1752 */ 1753 mergeOptions: function(options) { 1754 return $.extend(true, {}, this.defaults, this.options, options); 1755 }, 1756 1757 /** 1758 * Checks whether a select all checkbox is present. 1759 * 1760 * @returns {Boolean} 1761 */ 1762 hasSelectAll: function() { 1763 return $('li.multiselect-all', this.$ul).length > 0; 1764 }, 1765 1766 /** 1767 * Update opt groups. 1768 */ 1769 updateOptGroups: function() { 1770 var $groups = $('li.multiselect-group', this.$ul) 1771 var selectedClass = this.options.selectedClass; 1772 1773 $groups.each(function() { 1774 var $options = $(this).nextUntil('li.multiselect-group') 1775 .not('.multiselect-filter-hidden') 1776 .not('.disabled'); 1777 1778 var checked = true; 1779 $options.each(function() { 1780 var $input = $('input', this); 1781 1782 if (!$input.prop('checked')) { 1783 checked = false; 1784 } 1785 }); 1786 1787 if (selectedClass) { 1788 if (checked) { 1789 $(this).addClass(selectedClass); 1790 } 1791 else { 1792 $(this).removeClass(selectedClass); 1793 } 1794 } 1795 1796 $('input', this).prop('checked', checked); 1797 }); 1798 }, 1799 1800 /** 1801 * Updates the select all checkbox based on the currently displayed and selected checkboxes. 1802 */ 1803 updateSelectAll: function(notTriggerOnSelectAll) { 1804 if (this.hasSelectAll()) { 1805 var allBoxes = $("li:not(.multiselect-item):not(.multiselect-filter-hidden):not(.multiselect-group):not(.disabled) input:enabled", this.$ul); 1806 var allBoxesLength = allBoxes.length; 1807 var checkedBoxesLength = allBoxes.filter(":checked").length; 1808 var selectAllLi = $("li.multiselect-all", this.$ul); 1809 var selectAllInput = selectAllLi.find("input"); 1810 1811 if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) { 1812 selectAllInput.prop("checked", true); 1813 selectAllLi.addClass(this.options.selectedClass); 1814 } 1815 else { 1816 selectAllInput.prop("checked", false); 1817 selectAllLi.removeClass(this.options.selectedClass); 1818 } 1819 } 1820 }, 1821 1822 /** 1823 * Update the button text and its aria-label based on the currently selected options. 1824 */ 1825 updateButtonText: function() {
1826 var options = this.getSelected(); 1827 1828 // First update the displayed button text. 1829 if (this.options.enableHTML) { 1830 $('.multiselect .multiselect-selected-text', this.$container).html(this.options.buttonText(options, this.$select)); 1831 } 1832 else { 1833 $('.multiselect .multiselect-selected-text', this.$container).text(this.options.buttonText(options, this.$select)); 1834 } 1835 1836 // Now update the aria-label attribute of the button. 1837 $('.multiselect', this.$container).attr('aria-label', this.options.buttonTitle(options, this.$select)); 1838 }, 1839 1840 /** 1841 * Get all selected options. 1842 * 1843 * @returns {jQUery} 1844 */ 1845 getSelected: function() { 1846 return $('option', this.$select).filter(":selected"); 1847 }, 1848 1849 /** 1850 * Gets a select option by its value. 1851 * 1852 * @param {String} value 1853 * @returns {jQuery} 1854 */ 1855 getOptionByValue: function (value) { 1856 1857 var options = $('option', this.$select); 1858 var valueToCompare = value.toString(); 1859 1860 for (var i = 0; i < options.length; i = i + 1) { 1861 var option = options[i]; 1862 if (option.value === valueToCompare) { 1863 return $(option); 1864 } 1865 } 1866 }, 1867 1868 /** 1869 * Get the input (radio/checkbox) by its value. 1870 * 1871 * @param {String} value 1872 * @returns {jQuery} 1873 */ 1874 getInputByValue: function (value) { 1875 1876 var checkboxes = $('li input:not(.multiselect-search)', this.$ul); 1877 var valueToCompare = value.toString(); 1878 1879 for (var i = 0; i < checkboxes.length; i = i + 1) { 1880 var checkbox = checkboxes[i]; 1881 if (checkbox.value === valueToCompare) { 1882 return $(checkbox); 1883 } 1884 } 1885 }, 1886 1887 /** 1888 * Used for knockout integration. 1889 */ 1890 updateOriginalOptions: function() { 1891 this.originalOptions = this.$select.clone()[0].options; 1892 }, 1893 1894 asyncFunction: function(callback, timeout, self) { 1895 var args = Array.prototype.slice.call(arguments, 3); 1896 return setTimeout(function() { 1897 callback.apply(self || window, args); 1898 }, timeout); 1899 }, 1900 1901 setAllSelectedText: function(allSelectedText) { 1902 this.options.allSelectedText = allSelectedText; 1903 this.updateButtonText(); 1904 } 1905 }; 1906 1907 $.fn.multiselect = function(option, parameter, extraOptions) { 1908 return this.each(function() { 1909 var data = $(this).data('multiselect'); 1910 var options = typeof option === 'object' && option; 1911 1912 // Initialize the multiselect. 1913 if (!data) { 1914 data = new Multiselect(this, options); 1915 $(this).data('multiselect', data); 1916 } 1917 1918 // Call multiselect method. 1919 if (typeof option === 'string') { 1920 data[option](parameter, extraOptions); 1921 1922 if (option === 'destroy') { 1923 $(this).data('multiselect', false); 1924 } 1925 } 1926 }); 1927 }; 1928 1929 $.fn.multiselect.Constructor = Multiselect; 1930 1931 $(function() { 1932 $("select[data-role=multiselect]").multiselect(); 1933 }); 1934 1935});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.