PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.19
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.19
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / js / bootstrap-multiselect.js

bootstrap-multiselect.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.19, at js/bootstrap-multiselect.js

2,016 lines 79.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Bootstrap Multiselect (http://davidstutz.de/bootstrap-multiselect/)
3 *
4 * Apache License, Version 2.0:
5 * Copyright (c) 2012 - 2021 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 - 2021 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 this.buildButtons();
223
224 this.updateButtonText();
225 this.updateSelectAll(true);
226
227 if (this.options.enableClickableOptGroups && this.options.multiple) {
228 this.updateOptGroups();
229 }
230
231 this.options.wasDisabled = this.$select.prop('disabled');
232 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0 && !this.options.wasDisabled) {
233 this.disable(true);
234 }
235
236 this.$select.wrap('<span class="multiselect-native-select" />').after(this.$container);
237 this.$select.prop('tabindex', '-1');
238
239 if (this.options.widthSynchronizationMode !== 'never') {
240 this.synchronizeButtonAndPopupWidth();
241 }
242
243 this.options.onInitialized(this.$select, this.$container);
244 }
245
246 Multiselect.prototype = {
247
248 defaults: {
249 /**
250 * Default text function will either print 'None selected' in case no
251 * option is selected or a list of the selected options up to a length
252 * of 3 selected options.
253 *
254 * @param {jQuery} options
255 * @param {jQuery} select
256 * @returns {String}
257 */
258 buttonText: function (selectedOptions, select) {
259 if (this.disabledText.length > 0 && select.prop('disabled')) {
260 return this.disabledText;
261 }
262 else if (selectedOptions.length === 0) {
263 return this.nonSelectedText;
264 }
265 else if (this.allSelectedText
266 && selectedOptions.length === $('option', $(select)).length
267 && $('option', $(select)).length !== 1
268 && this.multiple) {
269
270 if (this.selectAllNumber) {
271 return this.allSelectedText + ' (' + selectedOptions.length + ')';
272 }
273 else {
274 return this.allSelectedText;
275 }
276 }
277 else if (this.numberDisplayed != 0 && selectedOptions.length > this.numberDisplayed) {
278 return selectedOptions.length + ' ' + this.nSelectedText;
279 }
280 else {
281 var selected = '';
282 var delimiter = this.delimiterText;
283
284 selectedOptions.each(function () {
285 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
286 selected += label + delimiter;
287 });
288
289 return selected.substr(0, selected.length - this.delimiterText.length);
290 }
291 },
292 /**
293 * Updates the title of the button similar to the buttonText function.
294 *
295 * @param {jQuery} options
296 * @param {jQuery} select
297 * @returns {@exp;selected@call;substr}
298 */
299 buttonTitle: function (options, select) {
300 if (options.length === 0) {
301 return this.nonSelectedText;
302 }
303 else {
304 var selected = '';
305 var delimiter = this.delimiterText;
306
307 options.each(function () {
308 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
309 selected += label + delimiter;
310 });
311 return selected.substr(0, selected.length - this.delimiterText.length);
312 }
313 },
314 checkboxName: function (option) {
315 return false; // no checkbox name
316 },
317 /**
318 * Create a label.
319 *
320 * @param {jQuery} element
321 * @returns {String}
322 */
323 optionLabel: function (element) {
324 return $(element).attr('label') || $(element).text();
325 },
326 /**
327 * Create a class.
328 *
329 * @param {jQuery} element
330 * @returns {String}
331 */
332 optionClass: function (element) {
333 return $(element).attr('class') || '';
334 },
335 /**
336 * Triggered on change of the multiselect.
337 *
338 * Not triggered when selecting/deselecting options manually.
339 *
340 * @param {jQuery} option
341 * @param {Boolean} checked
342 */
343 onChange: function (option, checked) {
344
345 },
346 /**
347 * Triggered when the dropdown is shown.
348 *
349 * @param {jQuery} event
350 */
351 onDropdownShow: function (event) {
352
353 },
354 /**
355 * Triggered when the dropdown is hidden.
356 *
357 * @param {jQuery} event
358 */
359 onDropdownHide: function (event) {
360
361 },
362 /**
363 * Triggered after the dropdown is shown.
364 *
365 * @param {jQuery} event
366 */
367 onDropdownShown: function (event) {
368
369 },
370 /**
371 * Triggered after the dropdown is hidden.
372 *
373 * @param {jQuery} event
374 */
375 onDropdownHidden: function (event) {
376
377 },
378 /**
379 * Triggered on select all.
380 */
381 onSelectAll: function () {
382
383 },
384 /**
385 * Triggered on deselect all.
386 */
387 onDeselectAll: function () {
388
389 },
390 /**
391 * Triggered after initializing.
392 *
393 * @param {jQuery} $select
394 * @param {jQuery} $container
395 */
396 onInitialized: function ($select, $container) {
397
398 },
399 /**
400 * Triggered on filtering.
401 *
402 * @param {jQuery} $filter
403 */
404 onFiltering: function ($filter) {
405
406 },
407 enableHTML: false,
408 buttonClass: 'custom-select',
409 inheritClass: false,
410 buttonWidth: 'auto',
411 buttonContainer: '<div class="btn-group" />',
412 dropRight: false,
413 dropUp: false,
414 selectedClass: 'active',
415 // Maximum height of the dropdown menu.
416 // If maximum height is exceeded a scrollbar will be displayed.
417 maxHeight: false,
418 includeSelectAllOption: false,
419 includeSelectAllIfMoreThan: 0,
420 selectAllText: ' Select all',
421 selectAllValue: 'multiselect-all',
422 selectAllName: false,
423 selectAllNumber: true,
424 selectAllJustVisible: true,
425 enableFiltering: false,
426 enableCaseInsensitiveFiltering: false,
427 enableFullValueFiltering: false,
428 enableClickableOptGroups: false,
429 enableCollapsibleOptGroups: false,
430 collapseOptGroupsByDefault: false,
431 filterPlaceholder: 'Search',
432 // possible options: 'text', 'value', 'both'
433 filterBehavior: 'text',
434 includeFilterClearBtn: true,
435 preventInputChangeEvent: false,
436 nonSelectedText: 'None selected',
437 nSelectedText: 'selected',
438 allSelectedText: 'All selected',
439 resetButtonText: 'Reset',
440 numberDisplayed: 3,
441 disableIfEmpty: false,
442 disabledText: '',
443 delimiterText: ', ',
444 includeResetOption: false,
445 includeResetDivider: false,
446 resetText: 'Reset',
447 indentGroupOptions: true,
448 // possible options: 'never', 'always', 'ifPopupIsSmaller', 'ifPopupIsWider'
449 widthSynchronizationMode: 'never',
450 buttonTextAlignment: 'center',
451 enableResetButton: false,
452 templates: {
453 button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span></button>',
454 popupContainer: '<div class="multiselect-container dropdown-menu"></div>',
455 filter: '<div class="multiselect-filter d-flex align-items-center"><i class="fas fa-sm fa-search text-muted"></i><input type="search" class="multiselect-search form-control" /></div>',
456 buttonGroup: '<div class="multiselect-buttons btn-group" style="display:flex;"></div>',
457 buttonGroupReset: '<button type="button" class="multiselect-reset btn btn-secondary btn-block"></button>',
458 option: '<button type="button" class="multiselect-option dropdown-item"></button>',
459 divider: '<div class="dropdown-divider"></div>',
460 optionGroup: '<button type="button" class="multiselect-group dropdown-item"></button>',
461 resetButton: '<div class="multiselect-reset text-center p-2"><button type="button" class="btn btn-sm btn-block btn-outline-secondary"></button></div>'
462 }
463 },
464
465 constructor: Multiselect,
466
467 /**
468 * Builds the container of the multiselect.
469 */
470 buildContainer: function () {
471 this.$container = $(this.options.buttonContainer);
472 if (this.options.widthSynchronizationMode !== 'never') {
473 this.$container.on('show.bs.dropdown', $.proxy(function () {
474 // the width needs to be synchronized again in case the width of the button changed in between
475 this.synchronizeButtonAndPopupWidth();
476 this.options.onDropdownShow();
477 }, this));
478 }
479 else {
480 this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
481 }
482 this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
483 this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
484 this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
485 },
486
487 /**
488 * Builds the button of the multiselect.
489 */
490 buildButton: function () {
491 this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
492 if (this.$select.attr('class') && this.options.inheritClass) {
493 this.$button.addClass(this.$select.attr('class'));
494 }
495 // Adopt active state.
496 if (this.$select.prop('disabled')) {
497 this.disable();
498 }
499 else {
500 this.enable();
501 }
502
503 // Manually add button width if set.
504 if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
505 this.$button.css({
506 'width': '100%' //this.options.buttonWidth,
507 });
508 this.$container.css({
509 'width': this.options.buttonWidth
510 });
511 }
512
513 if (this.options.buttonTextAlignment) {
514 switch (this.options.buttonTextAlignment) {
515 case 'left':
516 this.$button.addClass('text-left');
517 break;
518 case 'center':
519 this.$button.addClass('text-center');
520 break;
521 case 'right':
522 this.$button.addClass('text-right');
523 break;
524 }
525 }
526
527 // Keep the tab index from the select.
528 var tabindex = this.$select.attr('tabindex');
529 if (tabindex) {
530 this.$button.attr('tabindex', tabindex);
531 }
532
533 this.$container.prepend(this.$button);
534 },
535
536 /**
537 * Builds the popup container representing the dropdown menu.
538 */
539 buildDropdown: function () {
540
541 // Build popup container.
542 this.$popupContainer = $(this.options.templates.popupContainer);
543
544 if (this.options.dropRight) {
545 this.$container.addClass('dropright');
546 }
547 else if (this.options.dropUp) {
548 this.$container.addClass("dropup");
549 }
550
551 // Set max height of dropdown menu to activate auto scrollbar.
552 if (this.options.maxHeight) {
553 // TODO: Add a class for this option to move the css declarations.
554 this.$popupContainer.css({
555 'max-height': this.options.maxHeight + 'px',
556 'overflow-y': 'auto',
557 'overflow-x': 'hidden'
558 });
559 }
560
561 if (this.options.widthSynchronizationMode !== 'never') {
562 this.$popupContainer.css('overflow-x', 'hidden');
563 }
564
565 this.$popupContainer.on("touchstart click", function (e) {
566 e.stopPropagation();
567 });
568
569 this.$container.append(this.$popupContainer);
570 },
571
572 synchronizeButtonAndPopupWidth: function () {
573 if (!this.$popupContainer || this.options.widthSynchronizationMode === 'never') {
574 return;
575 }
576
577 var buttonWidth = this.$button.outerWidth();
578 switch (this.options.widthSynchronizationMode) {
579 case 'always':
580 this.$popupContainer.css('min-width', buttonWidth);
581 this.$popupContainer.css('max-width', buttonWidth);
582 break;
583 case 'ifPopupIsSmaller':
584 this.$popupContainer.css('min-width', buttonWidth);
585 break;
586 case 'ifPopupIsWider':
587 this.$popupContainer.css('max-width', buttonWidth);
588 break;
589 }
590 },
591
592 /**
593 * Build the dropdown options and binds all necessary events.
594 *
595 * Uses createDivider and createOptionValue to create the necessary options.
596 */
597 buildDropdownOptions: function () {
598
599 this.$select.children().each($.proxy(function (index, element) {
600
601 var $element = $(element);
602 // Support optgroups and options without a group simultaneously.
603 var tag = $element.prop('tagName')
604 .toLowerCase();
605
606 if ($element.prop('value') === this.options.selectAllValue) {
607 return;
608 }
609
610 if (tag === 'optgroup') {
611 this.createOptgroup(element);
612 }
613 else if (tag === 'option') {
614
615 if ($element.data('role') === 'divider') {
616 this.createDivider();
617 }
618 else {
619 this.createOptionValue(element, false);
620 }
621
622 }
623
624 // Other illegal tags will be ignored.
625 }, this));
626
627 // Bind the change event on the dropdown elements.
628 $(this.$popupContainer).off('change', '> *:not(.multiselect-group) input[type="checkbox"], > *:not(.multiselect-group) input[type="radio"]');
629 $(this.$popupContainer).on('change', '> *:not(.multiselect-group) input[type="checkbox"], > *:not(.multiselect-group) input[type="radio"]', $.proxy(function (event) {
630 var $target = $(event.target);
631
632 var checked = $target.prop('checked') || false;
633 var isSelectAllOption = $target.val() === this.options.selectAllValue;
634
635 // Apply or unapply the configured selected class.
636 if (this.options.selectedClass) {
637 if (checked) {
638 $target.closest('.multiselect-option')
639 .addClass(this.options.selectedClass);
640 }
641 else {
642 $target.closest('.multiselect-option')
643 .removeClass(this.options.selectedClass);
644 }
645 }
646
647 // Get the corresponding option.
648 var value = $target.val();
649 var $option = this.getOptionByValue(value);
650
651 var $optionsNotThis = $('option', this.$select).not($option);
652 var $checkboxesNotThis = $('input', this.$container).not($target);
653
654 if (isSelectAllOption) {
655
656 if (checked) {
657 this.selectAll(this.options.selectAllJustVisible, true);
658 }
659 else {
660 this.deselectAll(this.options.selectAllJustVisible, true);
661 }
662 }
663 else {
664 if (checked) {
665 $option.prop('selected', true);
666
667 if (this.options.multiple) {
668 // Simply select additional option.
669 $option.prop('selected', true);
670 }
671 else {
672 // Unselect all other options and corresponding checkboxes.
673 if (this.options.selectedClass) {
674 $($checkboxesNotThis).closest('.dropdown-item').removeClass(this.options.selectedClass);
675 }
676
677 $($checkboxesNotThis).prop('checked', false);
678 $optionsNotThis.prop('selected', false);
679
680 // It's a single selection, so close.
681 this.$button.click();
682 }
683
684 if (this.options.selectedClass === "active") {
685 $optionsNotThis.closest(".dropdown-item").css("outline", "");
686 }
687 }
688 else {
689 // Unselect option.
690 $option.prop('selected', false);
691 }
692
693 // To prevent select all from firing onChange: #575
694 this.options.onChange($option, checked);
695
696 // Do not update select all or optgroups on select all change!
697 this.updateSelectAll();
698
699 if (this.options.enableClickableOptGroups && this.options.multiple) {
700 this.updateOptGroups();
701 }
702 }
703
704 this.$select.change();
705 this.updateButtonText();
706
707 if (this.options.preventInputChangeEvent) {
708 return false;
709 }
710 }, this));
711
712 $('.multiselect-option', this.$popupContainer).off('mousedown');
713 $('.multiselect-option', this.$popupContainer).on('mousedown', function (e) {
714 if (e.shiftKey) {
715 // Prevent selecting text by Shift+click
716 return false;
717 }
718 });
719
720 $(this.$popupContainer).off('touchstart click', '.multiselect-option, .multiselect-all, .multiselect-group');
721 $(this.$popupContainer).on('touchstart click', '.multiselect-option, .multiselect-all, .multiselect-group', $.proxy(function (event) {
722 event.stopPropagation();
723
724 var $target = $(event.target);
725
726 if (event.shiftKey && this.options.multiple) {
727 if (!$target.is("input")) { // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
728 event.preventDefault();
729 $target = $target.closest(".multiselect-option").find("input");
730 $target.prop("checked", !$target.prop("checked"));
731 }
732 var checked = $target.prop('checked') || false;
733
734 if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
735 var from = this.$popupContainer.find(".multiselect-option:visible").index($target.closest(".multiselect-option"));
736 var to = this.$popupContainer.find(".multiselect-option:visible").index(this.lastToggledInput.closest(".multiselect-option"));
737
738 if (from > to) { // Swap the indices
739 var tmp = to;
740 to = from;
741 from = tmp;
742 }
743
744 // Make sure we grab all elements since slice excludes the last index
745 ++to;
746
747 // Change the checkboxes and underlying options
748 var range = this.$popupContainer.find(".multiselect-option:not(.multiselect-filter-hidden)").slice(from, to).find("input");
749
750 range.prop('checked', checked);
751
752 if (this.options.selectedClass) {
753 range.closest('.multiselect-option')
754 .toggleClass(this.options.selectedClass, checked);
755 }
756
757 for (var i = 0, j = range.length; i < j; i++) {
758 var $checkbox = $(range[i]);
759
760 var $option = this.getOptionByValue($checkbox.val());
761
762 $option.prop('selected', checked);
763 }
764 }
765
766 // Trigger the select "change" event
767 $target.trigger("change");
768 }
769 else if (!$target.is('input')) {
770 var $checkbox = $target.closest('.multiselect-option, .multiselect-all').find('.form-check-input');
771 if ($checkbox.length > 0) {
772 if (this.options.multiple || !$checkbox.prop('checked')) {
773 $checkbox.prop('checked', !$checkbox.prop('checked'));
774 $checkbox.change();
775 }
776 }
777 else if (this.options.enableClickableOptGroups && this.options.multiple && !$target.hasClass("caret-container")) {
778 var groupItem = $target;
779 if (!groupItem.hasClass("multiselect-group")) {
780 groupItem = $target.closest('.multiselect-group');
781 }
782 $checkbox = groupItem.find(".form-check-input");
783 if ($checkbox.length > 0) {
784 $checkbox.prop('checked', !$checkbox.prop('checked'));
785 $checkbox.change();
786 }
787 }
788
789 event.preventDefault();
790 }
791
792 // Remembers last clicked option
793 var $input = $target.closest(".multiselect-option").find("input[type='checkbox'], input[type='radio']");
794 if ($input.length > 0) {
795 this.lastToggledInput = $target;
796 }
797 else {
798 this.lastToggledInput = null;
799 }
800
801 $target.blur();
802 }, this));
803
804 //Keyboard support.
805 this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function (event) {
806 var $items = $(this.$container).find(".multiselect-option:not(.disabled), .multiselect-group:not(.disabled), .multiselect-all").filter(":visible");
807 var index = $items.index($items.filter(':focus'));
808 var $search = $('.multiselect-search', this.$container);
809
810 // keyCode 9 == Tab
811 if (event.keyCode === 9 && this.$container.hasClass('show')) {
812 this.$button.click();
813 }
814 // keyCode 13 = Enter
815 else if (event.keyCode == 13) {
816 var $current = $items.eq(index);
817 setTimeout(function () {
818 $current.focus();
819 }, 1);
820 }
821 // keyCode 38 = Arrow Up
822 else if (event.keyCode == 38) {
823 if (index == 0 && !$search.is(':focus')) {
824 setTimeout(function () {
825 $search.focus();
826 }, 1);
827 }
828 }
829 // keyCode 40 = Arrow Down
830 else if (event.keyCode == 40) {
831 if ($search.is(':focus')) {
832 var $first = $items.eq(0);
833 setTimeout(function () {
834 $search.blur();
835 $first.focus();
836 }, 1);
837 }
838 else if (index == -1) {
839 setTimeout(function () {
840 $search.focus();
841 }, 1);
842 }
843 }
844 }, this));
845
846 if (this.options.enableClickableOptGroups && this.options.multiple) {
847 $(".multiselect-group input", this.$popupContainer).off("change");
848 $(".multiselect-group input", this.$popupContainer).on("change", $.proxy(function (event) {
849 event.stopPropagation();
850
851 var $target = $(event.target);
852 var checked = $target.prop('checked') || false;
853
854 var $item = $(event.target).closest('.dropdown-item');
855 var $group = $item.nextUntil(".multiselect-group")
856 .not('.multiselect-filter-hidden')
857 .not('.disabled');
858
859 var $inputs = $group.find("input");
860
861 var $options = [];
862
863 if (this.options.selectedClass) {
864 if (checked) {
865 $item.addClass(this.options.selectedClass);
866 }
867 else {
868 $item.removeClass(this.options.selectedClass);
869 }
870 }
871
872 $.each($inputs, $.proxy(function (index, input) {
873 var $input = $(input);
874 var value = $input.val();
875 var $option = this.getOptionByValue(value);
876
877 if (checked) {
878 $input.prop('checked', true);
879 $input.closest('.dropdown-item')
880 .addClass(this.options.selectedClass);
881
882 $option.prop('selected', true);
883 }
884 else {
885 $input.prop('checked', false);
886 $input.closest('.dropdown-item')
887 .removeClass(this.options.selectedClass);
888
889 $option.prop('selected', false);
890 }
891
892 $options.push(this.getOptionByValue(value));
893 }, this))
894
895 // Cannot use select or deselect here because it would call updateOptGroups again.
896
897 this.options.onChange($options, checked);
898
899 this.$select.change();
900 this.updateButtonText();
901 this.updateSelectAll();
902 }, this));
903 }
904
905 if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
906 $(".multiselect-group .caret-container", this.$popupContainer).off("click");
907 $(".multiselect-group .caret-container", this.$popupContainer).on("click", $.proxy(function (event) {
908 var $group = $(event.target).closest('.multiselect-group');
909 var $inputs = $group.nextUntil(".multiselect-group")
910 .not('.multiselect-filter-hidden');
911
912 var visible = true;
913 $inputs.each(function () {
914 visible = visible && !$(this).hasClass('multiselect-collapsible-hidden');
915 });
916
917 if (visible) {
918 $inputs.hide()
919 .addClass('multiselect-collapsible-hidden');
920 }
921 else {
922 $inputs.show()
923 .removeClass('multiselect-collapsible-hidden');
924 }
925 }, this));
926 }
927 },
928
929 /**
930 * Create a checkbox container with input and label based on given values
931 * @param {JQuery} $item
932 * @param {String} label
933 * @param {String} name
934 * @param {String} value
935 * @param {String} inputType
936 * @returns {JQuery}
937 */
938 createCheckbox: function ($item, labelContent, name, value, title, inputType) {
939 var $wrapper = $('<span />');
940 $wrapper.addClass("form-check");
941
942 if (this.options.enableHTML && $(labelContent).length > 0) {
943 var $checkboxLabel = $('<label class="form-check-label" />');
944 $checkboxLabel.html(labelContent);
945 $wrapper.append($checkboxLabel);
946 }
947 else {
948 var $checkboxLabel = $('<label class="form-check-label" />');
949 $checkboxLabel.text(labelContent);
950 $wrapper.append($checkboxLabel);
951 }
952
953 var $checkbox = $('<input class="form-check-input"/>').attr('type', inputType);
954 $checkbox.val(value);
955 $wrapper.prepend($checkbox);
956
957 if (name) {
958 $checkbox.attr('name', name);
959 }
960
961 $item.prepend($wrapper);
962 $item.attr("title", title || labelContent);
963
964 return $checkbox;
965 },
966
967 /**
968 * Create an option using the given select option.
969 *
970 * @param {jQuery} element
971 */
972 createOptionValue: function (element, isGroupOption) {
973 var $element = $(element);
974 if ($element.is(':selected')) {
975 $element.prop('selected', true);
976 }
977
978 // Support the label attribute on options.
979 var label = this.options.optionLabel(element);
980 var classes = this.options.optionClass(element);
981 var value = $element.val();
982 var inputType = this.options.multiple ? "checkbox" : "radio";
983 var title = $element.attr('title');
984
985 var $option = $(this.options.templates.option);
986 $option.addClass(classes);
987
988 if (isGroupOption && this.options.indentGroupOptions) {
989 $option.addClass("multiselect-group-option-indented")
990 }
991
992 // Hide all children items when collapseOptGroupsByDefault is true
993 if (this.options.collapseOptGroupsByDefault && $(element).parent().prop("tagName").toLowerCase() === "optgroup") {
994 $option.addClass("multiselect-collapsible-hidden");
995 $option.hide();
996 }
997
998 var name = this.options.checkboxName($element);
999 var $checkbox = this.createCheckbox($option, label, name, value, title, inputType);
1000
1001 var selected = $element.prop('selected') || false;
1002
1003 if (value === this.options.selectAllValue) {
1004 $option.addClass("multiselect-all");
1005 $option.removeClass("multiselect-option");
1006 $checkbox.parent().parent()
1007 .addClass('multiselect-all');
1008 }
1009
1010 this.$popupContainer.append($option);
1011
1012 if ($element.is(':disabled')) {
1013 $checkbox.attr('disabled', 'disabled')
1014 .prop('disabled', true)
1015 .closest('.dropdown-item')
1016 .addClass('disabled');
1017 }
1018
1019 $checkbox.prop('checked', selected);
1020
1021 if (selected && this.options.selectedClass) {
1022 $checkbox.closest('.dropdown-item')
1023 .addClass(this.options.selectedClass);
1024 }
1025 },
1026
1027 /**
1028 * Creates a divider using the given select option.
1029 *
1030 * @param {jQuery} element
1031 */
1032 createDivider: function (element) {
1033 var $divider = $(this.options.templates.divider);
1034 this.$popupContainer.append($divider);
1035 },
1036
1037 /**
1038 * Creates an optgroup.
1039 *
1040 * @param {jQuery} group
1041 */
1042 createOptgroup: function (group) {
1043 var $group = $(group);
1044 var label = $group.attr("label");
1045 var value = $group.attr("value");
1046 var title = $group.attr('title');
1047
1048 var $groupOption = $("<span class='multiselect-group dropdown-item-text'></span>");
1049
1050 if (this.options.enableClickableOptGroups && this.options.multiple) {
1051 $groupOption = $(this.options.templates.optionGroup);
1052 var $checkbox = this.createCheckbox($groupOption, label, null, value, title, "checkbox");
1053 }
1054 else {
1055 if (this.options.enableHTML) {
1056 $groupOption.html(" " + label);
1057 }
1058 else {
1059 $groupOption.text(" " + label);
1060 }
1061 }
1062
1063 var classes = this.options.optionClass(group);
1064 $groupOption.addClass(classes);
1065
1066 if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
1067 $groupOption.find('.form-check').addClass('d-inline-block');
1068 $groupOption.append('<span class="caret-container dropdown-toggle pl-1"></span>');
1069 }
1070
1071 if ($group.is(':disabled')) {
1072 $groupOption.addClass('disabled');
1073 }
1074
1075 this.$popupContainer.append($groupOption);
1076
1077 $("option", group).each($.proxy(function ($, group) {
1078 this.createOptionValue(group, true);
1079 }, this));
1080 },
1081
1082 /**
1083 * Build the reset.
1084 *
1085 */
1086 buildReset: function () {
1087 if (this.options.includeResetOption) {
1088
1089 // Check whether to add a divider after the reset.
1090 if (this.options.includeResetDivider) {
1091 var divider = $(this.options.templates.divider);
1092 divider.addClass("mt-0");
1093 this.$popupContainer.prepend(divider);
1094 }
1095
1096 var $resetButton = $(this.options.templates.resetButton);
1097
1098 if (this.options.enableHTML) {
1099 $('button', $resetButton).html(this.options.resetText);
1100 }
1101 else {
1102 $('button', $resetButton).text(this.options.resetText);
1103 }
1104
1105 $('button', $resetButton).click($.proxy(function () {
1106 this.clearSelection();
1107 }, this));
1108
1109 this.$popupContainer.prepend($resetButton);
1110 }
1111 },
1112
1113 /**
1114 * Build the select all.
1115 *
1116 * Checks if a select all has already been created.
1117 */
1118 buildSelectAll: function () {
1119 if (typeof this.options.selectAllValue === 'number') {
1120 this.options.selectAllValue = this.options.selectAllValue.toString();
1121 }
1122
1123 var alreadyHasSelectAll = this.hasSelectAll();
1124
1125 if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
1126 && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) {
1127
1128 // Check whether to add a divider after the select all.
1129 if (this.options.includeSelectAllDivider) {
1130 this.$popupContainer.prepend($(this.options.templates.divider));
1131 }
1132
1133 var $option = $(this.options.templates.li || this.options.templates.option);
1134 var $checkbox = this.createCheckbox($option, this.options.selectAllText, this.options.selectAllName, this.options.selectAllValue, this.options.selectAllText, "checkbox");
1135
1136 $option.addClass("multiselect-all");
1137 $option.removeClass("multiselect-option");
1138 $option.find(".form-check-label").addClass("font-weight-bold");
1139
1140 this.$popupContainer.prepend($option);
1141
1142 $checkbox.prop('checked', false);
1143 }
1144 },
1145
1146 /**
1147 * Builds the filter.
1148 */
1149 buildFilter: function () {
1150
1151 // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
1152 if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
1153 var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
1154
1155 if (this.$select.find('option').length >= enableFilterLength) {
1156
1157 this.$filter = $(this.options.templates.filter);
1158 $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
1159
1160 // Handles optional filter clear button
1161 if (!this.options.includeFilterClearBtn) {
1162 this.$filter.find(".multiselect-search").attr("type", "text");
1163
1164 // Remove clear button if the old design of the filter with input groups and separated clear button is used
1165 this.$filter.find(".multiselect-clear-filter").remove();
1166 }
1167 else {
1168 // Firefox does not support a clear button in search inputs right now therefore it must be added manually
1169 if (this.isFirefox() && this.$filter.find(".multiselect-clear-filter").length === 0) {
1170 this.$filter.append("<i class='fas fa-times text-muted multiselect-clear-filter multiselect-moz-clear-filter'></i>");
1171 }
1172
1173 this.$filter.find(".multiselect-clear-filter").on('click', $.proxy(function (event) {
1174 clearTimeout(this.searchTimeout);
1175
1176 this.query = '';
1177 this.$filter.find('.multiselect-search').val('');
1178 $('.dropdown-item', this.$popupContainer).show().removeClass('multiselect-filter-hidden');
1179
1180 this.updateSelectAll();
1181
1182 if (this.options.enableClickableOptGroups && this.options.multiple) {
1183 this.updateOptGroups();
1184 }
1185
1186 }, this));
1187 }
1188
1189 this.$popupContainer.prepend(this.$filter);
1190
1191 this.$filter.val(this.query).on('click', function (event) {
1192 event.stopPropagation();
1193 }).on('input keydown', $.proxy(function (event) {
1194 // Cancel enter key default behaviour
1195 if (event.which === 13) {
1196 event.preventDefault();
1197 }
1198
1199 if (this.isFirefox() && this.options.includeFilterClearBtn) {
1200 if (event.target.value) {
1201 this.$filter.find(".multiselect-moz-clear-filter").show();
1202 }
1203 else {
1204 this.$filter.find(".multiselect-moz-clear-filter").hide();
1205 }
1206 }
1207
1208 // This is useful to catch "keydown" events after the browser has updated the control.
1209 clearTimeout(this.searchTimeout);
1210
1211 this.searchTimeout = this.asyncFunction($.proxy(function () {
1212
1213 if (this.query !== event.target.value) {
1214 this.query = event.target.value;
1215
1216 var currentGroup, currentGroupVisible;
1217 $.each($('.multiselect-option, .multiselect-group', this.$popupContainer), $.proxy(function (index, element) {
1218 var value = $('input', element).length > 0 ? $('input', element).val() : "";
1219 var text = $('.form-check-label', element).text();
1220
1221 var filterCandidate = '';
1222 if ((this.options.filterBehavior === 'text')) {
1223 filterCandidate = text;
1224 }
1225 else if ((this.options.filterBehavior === 'value')) {
1226 filterCandidate = value;
1227 }
1228 else if (this.options.filterBehavior === 'both') {
1229 filterCandidate = text + '\n' + value;
1230 }
1231
1232 if (value !== this.options.selectAllValue && text) {
1233
1234 // By default lets assume that element is not
1235 // interesting for this search.
1236 var showElement = false;
1237
1238 if (this.options.enableCaseInsensitiveFiltering) {
1239 filterCandidate = filterCandidate.toLowerCase();
1240 this.query = this.query.toLowerCase();
1241 }
1242
1243 if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') {
1244 var valueToMatch = filterCandidate.trim().substring(0, this.query.length);
1245 if (this.query.indexOf(valueToMatch) > -1) {
1246 showElement = true;
1247 }
1248 }
1249 else if (filterCandidate.indexOf(this.query) > -1) {
1250 showElement = true;
1251 }
1252
1253 // Toggle current element (group or group item) according to showElement boolean.
1254 if (!showElement) {
1255 $(element).css('display', 'none');
1256 $(element).addClass('multiselect-filter-hidden');
1257 }
1258 if (showElement) {
1259 $(element).css('display', 'block');
1260 $(element).removeClass('multiselect-filter-hidden');
1261 }
1262
1263 // Differentiate groups and group items.
1264 if ($(element).hasClass('multiselect-group')) {
1265 // Remember group status.
1266 currentGroup = element;
1267 currentGroupVisible = showElement;
1268 }
1269 else {
1270 // Show group name when at least one of its items is visible.
1271 if (showElement) {
1272 $(currentGroup).show()
1273 .removeClass('multiselect-filter-hidden');
1274 }
1275
1276 // Show all group items when group name satisfies filter.
1277 if (!showElement && currentGroupVisible) {
1278 $(element).show()
1279 .removeClass('multiselect-filter-hidden');
1280 }
1281 }
1282 }
1283 }, this));
1284 }
1285
1286 this.updateSelectAll();
1287
1288 if (this.options.enableClickableOptGroups && this.options.multiple) {
1289 this.updateOptGroups();
1290 }
1291
1292 this.updatePopupPosition();
1293
1294 this.options.onFiltering(event.target);
1295
1296 }, this), 300, this);
1297 }, this));
1298 }
1299 }
1300 },
1301
1302 /**
1303 * Builds the filter.
1304 */
1305 buildButtons: function () {
1306 if (this.options.enableResetButton) {
1307 var $buttonGroup = $(this.options.templates.buttonGroup);
1308 this.$buttonGroupReset = $(this.options.templates.buttonGroupReset).text(this.options.resetButtonText);
1309 $buttonGroup.append(this.$buttonGroupReset);
1310 this.$popupContainer.prepend($buttonGroup);
1311
1312 // We save all options that were previously selected.
1313 this.defaultSelection = {};
1314 $('option', this.$select).each($.proxy(function(index, element) {
1315 var $option = $(element);
1316 this.defaultSelection[$option.val()] = $option.prop('selected');
1317 }, this));
1318
1319 this.$buttonGroupReset.on('click', $.proxy(function(event) {
1320 $('option', this.$select).each($.proxy(function(index, element) {
1321 var $option = $(element);
1322 $option.prop('selected', this.defaultSelection[$option.val()]);
1323 }, this));
1324 this.refresh();
1325
1326 if (this.options.enableFiltering) {
1327 this.$filter.trigger('keydown');
1328 $('input', this.$filter).val('');
1329 }
1330 }, this));
1331 }
1332 },
1333
1334 updatePopupPosition: function() {
1335 // prevent gaps between popup and select when filter is used (#1199)
1336 var transformMatrix = this.$popupContainer.css("transform");
1337 var matrixType = transformMatrix.substring(0, transformMatrix.indexOf('('));
1338 var values = transformMatrix.substring(transformMatrix.indexOf('(') + 1, transformMatrix.length - 1);
1339 var valuesArray = values.split(',');
1340
1341 var valueIndex = 5;
1342 if(matrixType === "matrix3d") {
1343 valueIndex = 13;
1344 }
1345
1346 var yTransformation = valuesArray[valueIndex];
1347 // Need to check to avoid errors when testing and in some other situations.
1348 yTransformation = typeof yTransformation === 'undefined' ? 0 : yTransformation.trim();
1349 if (yTransformation < 0) {
1350 yTransformation = this.$popupContainer.css("height").replace('px', '') * -1;
1351 valuesArray[valueIndex] = yTransformation;
1352 transformMatrix = matrixType + '(' + valuesArray.join(',') + ')';
1353 this.$popupContainer.css("transform", transformMatrix);
1354 }
1355 },
1356
1357 /**
1358 * Unbinds the whole plugin.
1359 */
1360 destroy: function () {
1361 this.$container.remove();
1362 this.$select.unwrap();
1363 this.$select.show();
1364
1365 // reset original state
1366 this.$select.prop('disabled', this.options.wasDisabled);
1367
1368 this.$select.data('multiselect', null);
1369 },
1370
1371 /**
1372 * Refreshs the multiselect based on the selected options of the select.
1373 */
1374 refresh: function () {
1375 var inputs = {};
1376 $('.multiselect-option input', this.$popupContainer).each(function () {
1377 inputs[$(this).val()] = $(this);
1378 });
1379
1380 $('option', this.$select).each($.proxy(function (index, element) {
1381 var $elem = $(element);
1382 var $input = inputs[$(element).val()];
1383
1384 if ($elem.is(':selected')) {
1385 $input.prop('checked', true);
1386
1387 if (this.options.selectedClass) {
1388 $input.closest('.multiselect-option')
1389 .addClass(this.options.selectedClass);
1390 }
1391 }
1392 else {
1393 $input.prop('checked', false);
1394
1395 if (this.options.selectedClass) {
1396 $input.closest('.multiselect-option')
1397 .removeClass(this.options.selectedClass);
1398 }
1399 }
1400
1401 if ($elem.is(":disabled")) {
1402 $input.attr('disabled', 'disabled')
1403 .prop('disabled', true)
1404 .closest('.multiselect-option')
1405 .addClass('disabled');
1406 }
1407 else {
1408 $input.prop('disabled', false)
1409 .closest('.multiselect-option')
1410 .removeClass('disabled');
1411 }
1412 }, this));
1413
1414 this.updateButtonText();
1415 this.updateSelectAll();
1416
1417 if (this.options.enableClickableOptGroups && this.options.multiple) {
1418 this.updateOptGroups();
1419 }
1420 },
1421
1422 /**
1423 * Select all options of the given values.
1424 *
1425 * If triggerOnChange is set to true, the on change event is triggered if
1426 * and only if one value is passed.
1427 *
1428 * @param {Array} selectValues
1429 * @param {Boolean} triggerOnChange
1430 */
1431 select: function (selectValues, triggerOnChange) {
1432 if (!$.isArray(selectValues)) {
1433 selectValues = [selectValues];
1434 }
1435
1436 for (var i = 0; i < selectValues.length; i++) {
1437 var value = selectValues[i];
1438
1439 if (value === null || value === undefined) {
1440 continue;
1441 }
1442
1443 var $option = this.getOptionByValue(value);
1444 var $checkbox = this.getInputByValue(value);
1445
1446 if ($option === undefined || $checkbox === undefined) {
1447 continue;
1448 }
1449
1450 if (this.options.selectedClass) {
1451 $checkbox.closest('.dropdown-item')
1452 .addClass(this.options.selectedClass);
1453 }
1454
1455 $checkbox.prop('checked', true);
1456 $option.prop('selected', true);
1457
1458 if (!this.options.multiple) {
1459 var $checkboxesNotThis = $('input', this.$container).not($checkbox);
1460 $($checkboxesNotThis).prop('checked', false);
1461 $($checkboxesNotThis).closest('.multiselect-option').removeClass("active")
1462
1463 var $optionsNotThis = $('option', this.$select).not($option);
1464 $optionsNotThis.prop('selected', false);
1465 }
1466
1467 if (triggerOnChange) {
1468 this.options.onChange($option, true);
1469 }
1470 }
1471
1472 this.updateButtonText();
1473 this.updateSelectAll();
1474
1475 if (this.options.enableClickableOptGroups && this.options.multiple) {
1476 this.updateOptGroups();
1477 }
1478 },
1479
1480 /**
1481 * Clears all selected items.
1482 */
1483 clearSelection: function () {
1484 this.deselectAll(false);
1485 this.updateButtonText();
1486 this.updateSelectAll();
1487
1488 if (this.options.enableClickableOptGroups && this.options.multiple) {
1489 this.updateOptGroups();
1490 }
1491 },
1492
1493 /**
1494 * Deselects all options of the given values.
1495 *
1496 * If triggerOnChange is set to true, the on change event is triggered, if
1497 * and only if one value is passed.
1498 *
1499 * @param {Array} deselectValues
1500 * @param {Boolean} triggerOnChange
1501 */
1502 deselect: function (deselectValues, triggerOnChange) {
1503 if (!this.options.multiple) {
1504 // In single selection mode at least on option needs to be selected
1505 return;
1506 }
1507
1508 if (!$.isArray(deselectValues)) {
1509 deselectValues = [deselectValues];
1510 }
1511
1512 for (var i = 0; i < deselectValues.length; i++) {
1513 var value = deselectValues[i];
1514
1515 if (value === null || value === undefined) {
1516 continue;
1517 }
1518
1519 var $option = this.getOptionByValue(value);
1520 var $checkbox = this.getInputByValue(value);
1521
1522 if ($option === undefined || $checkbox === undefined) {
1523 continue;
1524 }
1525
1526 if (this.options.selectedClass) {
1527 $checkbox.closest('.dropdown-item')
1528 .removeClass(this.options.selectedClass);
1529 }
1530
1531 $checkbox.prop('checked', false);
1532 $option.prop('selected', false);
1533
1534 if (triggerOnChange) {
1535 this.options.onChange($option, false);
1536 }
1537 }
1538
1539 this.updateButtonText();
1540 this.updateSelectAll();
1541
1542 if (this.options.enableClickableOptGroups && this.options.multiple) {
1543 this.updateOptGroups();
1544 }
1545 },
1546
1547 /**
1548 * Selects all enabled & visible options.
1549 *
1550 * If justVisible is true or not specified, only visible options are selected.
1551 *
1552 * @param {Boolean} justVisible
1553 * @param {Boolean} triggerOnSelectAll
1554 */
1555 selectAll: function (justVisible, triggerOnSelectAll) {
1556 if (!this.options.multiple) {
1557 // In single selection mode only one option can be selected at a time
1558 return;
1559 }
1560
1561 // Record all changes, i.e., options selected that were not selected before.
1562 var selected = [];
1563 var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1564
1565 if (justVisible) {
1566 var visibleOptions = $(".multiselect-option:not(.disabled):not(.multiselect-filter-hidden)", this.$popupContainer);
1567 $('input:enabled', visibleOptions).prop('checked', true);
1568 visibleOptions.addClass(this.options.selectedClass);
1569
1570 $('input:enabled', visibleOptions).each($.proxy(function (index, element) {
1571 var value = $(element).val();
1572 var option = this.getOptionByValue(value);
1573 if (!$(option).prop('selected')) {
1574 selected.push(option);
1575 }
1576 $(option).prop('selected', true);
1577 }, this));
1578 }
1579 else {
1580 var allOptions = $(".multiselect-option:not(.disabled)", this.$popupContainer);
1581 $('input:enabled', allOptions).prop('checked', true);
1582 allOptions.addClass(this.options.selectedClass);
1583
1584 $('input:enabled', allOptions).each($.proxy(function (index, element) {
1585 var value = $(element).val();
1586 var option = this.getOptionByValue(value);
1587 if (!$(option).prop('selected')) {
1588 selected.push(option);
1589 }
1590 $(option).prop('selected', true);
1591 }, this));
1592 }
1593
1594 $('.multiselect-option input[value="' + this.options.selectAllValue + '"]', this.$popupContainer).prop('checked', true);
1595
1596 if (this.options.enableClickableOptGroups && this.options.multiple) {
1597 this.updateOptGroups();
1598 }
1599
1600 this.updateButtonText();
1601 this.updateSelectAll();
1602
1603 if (triggerOnSelectAll) {
1604 this.options.onSelectAll(selected);
1605 }
1606 },
1607
1608 /**
1609 * Deselects all options.
1610 *
1611 * If justVisible is true or not specified, only visible options are deselected.
1612 *
1613 * @param {Boolean} justVisible
1614 */
1615 deselectAll: function (justVisible, triggerOnDeselectAll) {
1616 if (!this.options.multiple) {
1617 // In single selection mode at least on option needs to be selected
1618 return;
1619 }
1620
1621 // Record changes, i.e., those options that are deselected but were not deselected before.
1622 var deselected = [];
1623 var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1624
1625 if (justVisible) {
1626 var visibleOptions = $(".multiselect-option:not(.disabled):not(.multiselect-filter-hidden)", this.$popupContainer);
1627 $('input[type="checkbox"]:enabled', visibleOptions).prop('checked', false);
1628 visibleOptions.removeClass(this.options.selectedClass);
1629
1630 $('input[type="checkbox"]:enabled', visibleOptions).each($.proxy(function (index, element) {
1631 var value = $(element).val();
1632 var option = this.getOptionByValue(value);
1633 if ($(option).prop('selected')) {
1634 deselected.push(option);
1635 }
1636 $(option).prop('selected', false);
1637 }, this));
1638 }
1639 else {
1640 var allOptions = $(".multiselect-option:not(.disabled):not(.multiselect-group)", this.$popupContainer);
1641 $('input[type="checkbox"]:enabled', allOptions).prop('checked', false);
1642 allOptions.removeClass(this.options.selectedClass);
1643
1644 $('input[type="checkbox"]:enabled', allOptions).each($.proxy(function (index, element) {
1645 var value = $(element).val();
1646 var option = this.getOptionByValue(value);
1647 if ($(option).prop('selected')) {
1648 deselected.push(option);
1649 }
1650 $(option).prop('selected', false);
1651 }, this));
1652 }
1653
1654 $('.multiselect-all input[value="' + this.options.selectAllValue + '"]', this.$popupContainer).prop('checked', false);
1655
1656 if (this.options.enableClickableOptGroups && this.options.multiple) {
1657 this.updateOptGroups();
1658 }
1659
1660 this.updateButtonText();
1661 this.updateSelectAll();
1662
1663 if (triggerOnDeselectAll) {
1664 this.options.onDeselectAll(deselected);
1665 }
1666 },
1667
1668 /**
1669 * Rebuild the plugin.
1670 *
1671 * Rebuilds the dropdown, the filter and the select all option.
1672 */
1673 rebuild: function () {
1674 this.$popupContainer.html('');
1675
1676 // Important to distinguish between radios and checkboxes.
1677 this.options.multiple = this.$select.attr('multiple') === "multiple";
1678
1679 this.buildSelectAll();
1680 this.buildDropdownOptions();
1681 this.buildFilter();
1682 this.buildButtons();
1683
1684 this.updateButtonText();
1685 this.updateSelectAll(true);
1686
1687 if (this.options.enableClickableOptGroups && this.options.multiple) {
1688 this.updateOptGroups();
1689 }
1690
1691 if (this.options.disableIfEmpty) {
1692 if ($('option', this.$select).length <= 0) {
1693 if (!this.$select.prop('disabled')) {
1694 this.disable(true);
1695 }
1696 }
1697 else if (this.$select.data("disabled-by-option")) {
1698 this.enable();
1699 }
1700 }
1701
1702 if (this.options.dropRight) {
1703 this.$container.addClass('dropright');
1704 }
1705 else if (this.options.dropUp) {
1706 this.$container.addClass('dropup');
1707 }
1708
1709 if (this.options.widthSynchronizationMode !== 'never') {
1710 this.synchronizeButtonAndPopupWidth();
1711 }
1712 },
1713
1714 /**
1715 * The provided data will be used to build the dropdown.
1716 */
1717 dataprovider: function (dataprovider) {
1718
1719 var groupCounter = 0;
1720 var $select = this.$select.empty();
1721
1722 $.each(dataprovider, function (index, option) {
1723 var $tag;
1724
1725 if ($.isArray(option.children)) { // create optiongroup tag
1726 groupCounter++;
1727
1728 $tag = $('<optgroup/>').attr({
1729 label: option.label || 'Group ' + groupCounter,
1730 disabled: !!option.disabled,
1731 value: option.value
1732 });
1733
1734 forEach(option.children, function (subOption) { // add children option tags
1735 var attributes = {
1736 value: subOption.value,
1737 label: subOption.label || subOption.value,
1738 title: subOption.title,
1739 selected: !!subOption.selected,
1740 disabled: !!subOption.disabled
1741 };
1742
1743 //Loop through attributes object and add key-value for each attribute
1744 for (var key in subOption.attributes) {
1745 attributes['data-' + key] = subOption.attributes[key];
1746 }
1747 //Append original attributes + new data attributes to option
1748 $tag.append($('<option/>').attr(attributes));
1749 });
1750 }
1751 else {
1752
1753 var attributes = {
1754 'value': option.value,
1755 'label': option.label || option.value,
1756 'title': option.title,
1757 'class': option['class'],
1758 'selected': !!option['selected'],
1759 'disabled': !!option['disabled']
1760 };
1761 //Loop through attributes object and add key-value for each attribute
1762 for (var key in option.attributes) {
1763 attributes['data-' + key] = option.attributes[key];
1764 }
1765 //Append original attributes + new data attributes to option
1766 $tag = $('<option/>').attr(attributes);
1767
1768 $tag.text(option.label || option.value);
1769 }
1770
1771 $select.append($tag);
1772 });
1773
1774 this.rebuild();
1775 },
1776
1777 /**
1778 * Enable the multiselect.
1779 */
1780 enable: function () {
1781 this.$select.prop('disabled', false);
1782 this.$button.prop('disabled', false)
1783 .removeClass('disabled');
1784
1785 this.updateButtonText();
1786 },
1787
1788 /**
1789 * Disable the multiselect.
1790 */
1791 disable: function (disableByOption) {
1792 this.$select.prop('disabled', true);
1793 this.$button.prop('disabled', true)
1794 .addClass('disabled');
1795
1796 if (disableByOption) {
1797 this.$select.data("disabled-by-option", true);
1798 }
1799 else {
1800 this.$select.data("disabled-by-option", null);
1801 }
1802
1803 this.updateButtonText();
1804 },
1805
1806 /**
1807 * Set the options.
1808 *
1809 * @param {Array} options
1810 */
1811 setOptions: function (options) {
1812 this.options = this.mergeOptions(options);
1813 },
1814
1815 /**
1816 * Merges the given options with the default options.
1817 *
1818 * @param {Array} options
1819 * @returns {Array}
1820 */
1821 mergeOptions: function (options) {
1822 return $.extend(true, {}, this.defaults, this.options, options);
1823 },
1824
1825 /**
1826 * Checks whether a select all checkbox is present.
1827 *
1828 * @returns {Boolean}
1829 */
1830 hasSelectAll: function () {
1831 return $('.multiselect-all', this.$popupContainer).length > 0;
1832 },
1833
1834 /**
1835 * Update opt groups.
1836 */
1837 updateOptGroups: function () {
1838 var $groups = $('.multiselect-group', this.$popupContainer)
1839 var selectedClass = this.options.selectedClass;
1840
1841 $groups.each(function () {
1842 var $options = $(this).nextUntil('.multiselect-group')
1843 .not('.multiselect-filter-hidden')
1844 .not('.disabled');
1845
1846 var checked = true;
1847 $options.each(function () {
1848 var $input = $('input', this);
1849
1850 if (!$input.prop('checked')) {
1851 checked = false;
1852 }
1853 });
1854
1855 if (selectedClass) {
1856 if (checked) {
1857 $(this).addClass(selectedClass);
1858 }
1859 else {
1860 $(this).removeClass(selectedClass);
1861 }
1862 }
1863
1864 $('input', this).prop('checked', checked);
1865 });
1866 },
1867
1868 /**
1869 * Updates the select all checkbox based on the currently displayed and selected checkboxes.
1870 */
1871 updateSelectAll: function (notTriggerOnSelectAll) {
1872 if (this.hasSelectAll()) {
1873 var allBoxes = $(".multiselect-option:not(.multiselect-filter-hidden):not(.multiselect-group):not(.disabled) input:enabled", this.$popupContainer);
1874 var allBoxesLength = allBoxes.length;
1875 var checkedBoxesLength = allBoxes.filter(":checked").length;
1876 var selectAllItem = $(".multiselect-all", this.$popupContainer);
1877 var selectAllInput = selectAllItem.find("input");
1878
1879 if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) {
1880 selectAllInput.prop("checked", true);
1881 selectAllItem.addClass(this.options.selectedClass);
1882 }
1883 else {
1884 selectAllInput.prop("checked", false);
1885 selectAllItem.removeClass(this.options.selectedClass);
1886 }
1887 }
1888 },
1889
1890 /**
1891 * Update the button text and its title based on the currently selected options.
1892 */
1893 updateButtonText: function () {
1894 var options = this.getSelected();
1895
1896 // First update the displayed button text.
1897 if (this.options.enableHTML) {
1898 $('.multiselect .multiselect-selected-text', this.$container).html(this.options.buttonText(options, this.$select));
1899 }
1900 else {
1901 $('.multiselect .multiselect-selected-text', this.$container).text(this.options.buttonText(options, this.$select));
1902 }
1903
1904 // Now update the title attribute of the button.
1905 $('.multiselect', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
1906 this.$button.trigger('change');
1907 },
1908
1909 /**
1910 * Get all selected options.
1911 *
1912 * @returns {jQUery}
1913 */
1914 getSelected: function () {
1915 return $('option', this.$select).filter(":selected");
1916 },
1917
1918 /**
1919 * Gets a select option by its value.
1920 *
1921 * @param {String} value
1922 * @returns {jQuery}
1923 */
1924 getOptionByValue: function (value) {
1925
1926 var options = $('option', this.$select);
1927 var valueToCompare = value.toString();
1928
1929 for (var i = 0; i < options.length; i = i + 1) {
1930 var option = options[i];
1931 if (option.value === valueToCompare) {
1932 return $(option);
1933 }
1934 }
1935 },
1936
1937 /**
1938 * Get the input (radio/checkbox) by its value.
1939 *
1940 * @param {String} value
1941 * @returns {jQuery}
1942 */
1943 getInputByValue: function (value) {
1944
1945 var checkboxes = $('.multiselect-option input:not(.multiselect-search)', this.$popupContainer);
1946 var valueToCompare = value.toString();
1947
1948 for (var i = 0; i < checkboxes.length; i = i + 1) {
1949 var checkbox = checkboxes[i];
1950 if (checkbox.value === valueToCompare) {
1951 return $(checkbox);
1952 }
1953 }
1954 },
1955
1956 /**
1957 * Used for knockout integration.
1958 */
1959 updateOriginalOptions: function () {
1960 this.originalOptions = this.$select.clone()[0].options;
1961 },
1962
1963 asyncFunction: function (callback, timeout, self) {
1964 var args = Array.prototype.slice.call(arguments, 3);
1965 return setTimeout(function () {
1966 callback.apply(self || window, args);
1967 }, timeout);
1968 },
1969
1970 setAllSelectedText: function (allSelectedText) {
1971 this.options.allSelectedText = allSelectedText;
1972 this.updateButtonText();
1973 },
1974
1975 isFirefox: function () {
1976 var firefoxIdentifier = 'firefox';
1977 var valueNotFoundIndex = -1;
1978
1979 if (navigator && navigator.userAgent) {
1980 return navigator.userAgent.toLocaleLowerCase().indexOf(firefoxIdentifier) > valueNotFoundIndex;
1981 }
1982
1983 return false;
1984 }
1985 };
1986
1987 $.fn.multiselect = function (option, parameter, extraOptions) {
1988 return this.each(function () {
1989 var data = $(this).data('multiselect');
1990 var options = typeof option === 'object' && option;
1991
1992 // Initialize the multiselect.
1993 if (!data) {
1994 data = new Multiselect(this, options);
1995 $(this).data('multiselect', data);
1996 }
1997
1998 // Call multiselect method.
1999 if (typeof option === 'string') {
2000 data[option](parameter, extraOptions);
2001
2002 if (option === 'destroy') {
2003 $(this).data('multiselect', false);
2004 }
2005 }
2006 });
2007 };
2008
2009 $.fn.multiselect.Constructor = Multiselect;
2010
2011 $(function () {
2012 $("select[data-role=multiselect]").multiselect();
2013 });
2014
2015 });
2016